mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-05-28 06:29:13 +02:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e8c1355ad | |||
| 26b1464381 | |||
| d7580d2271 | |||
| 8a1447f543 | |||
| 53c81c4596 | |||
| d3779e5a74 | |||
| c84fa50fc9 | |||
| 41fc316a59 | |||
| 011bcb8bd4 | |||
| dce3c91c22 | |||
| a9a1e68169 | |||
| cb9a7542a6 | |||
| 145a0ae003 | |||
| 96e87d8ae0 | |||
| 529d93595e | |||
| ea35d818a1 | |||
| 5f629e1f1a | |||
| 0f1854db8b | |||
| 8889c3ca93 | |||
| 6ca49d4301 | |||
| 634a3e7f3b | |||
| 5b6fab0044 | |||
| bed314b866 | |||
| c165172b5a | |||
| 9977cca500 | |||
| 5d7a0ea9ad | |||
| 0e79c58be3 | |||
| 09205bfd83 | |||
| 98122bd9d4 | |||
| 9741b387a7 | |||
| 1dad13106a | |||
| 0b5141e7a4 | |||
| 22e8a79833 | |||
| 5946caaf92 | |||
| 01d73b7d19 | |||
| 9d16197825 | |||
| fed09689b8 | |||
| c0b298c9de | |||
| 5fd7017b71 | |||
| dbde351e22 | |||
| 95838fc896 | |||
| bdef1448e1 | |||
| 0b39551470 | |||
| ab528f799d | |||
| 8805fafa99 | |||
| 1219c3d118 | |||
| b792328676 | |||
| a7380db47c | |||
| 086993a7c0 | |||
| 51e1337f40 | |||
| 6cef8d9ef6 | |||
| 6e36b81669 | |||
| 0758c7d900 | |||
| f77a303b30 | |||
| 3a81f7babb | |||
| 3d8778a7d6 | |||
| a42e809ade | |||
| 66c5766848 | |||
| ac56590791 | |||
| 4d67f5fed3 | |||
| c91224295f | |||
| 4b1e3bd448 | |||
| 1cb8ed7f10 | |||
| 90271f1f99 | |||
| 9fde9ed6f8 | |||
| ce4479eb9f | |||
| f3b7f5ac42 | |||
| 382896dfd2 | |||
| ff4187aecf | |||
| 8f982e45d1 | |||
| 309bfdfee1 | |||
| a0239faea0 | |||
| 999dd5d20d | |||
| 69086299e1 | |||
| 4114b10b05 | |||
| 71402fe01b | |||
| 5c2767d589 | |||
| ac67c41377 | |||
| faed6ec535 | |||
| 4ebba08b7d | |||
| a733fecb49 | |||
| 31c969fc55 | |||
| df2adde13d | |||
| a77b3e774a | |||
| c8c4c3507c | |||
| df38facf9e | |||
| 416dd94670 | |||
| d0ae92ca3a | |||
| 3491dc7590 | |||
| adbb6c1cb0 | |||
| 18ee667896 | |||
| 1a42c64620 | |||
| b613986474 | |||
| e93751aa5f | |||
| 9872dcd0d7 | |||
| f99c82f5d4 | |||
| b255058467 | |||
| 2abe618354 | |||
| e953715fef | |||
| 2ac1929117 | |||
| e33a5f28f0 | |||
| 15d069f2bf | |||
| 3e9947c2e2 | |||
| 1c23439f95 | |||
| 6eb2905e00 | |||
| 52a745d0a3 | |||
| 80877cc449 | |||
| 160cb7202d | |||
| 8d085df1ba | |||
| 5d48a5c5b4 | |||
| 539aeec965 | |||
| bcfb058bc3 | |||
| e07b464b0d |
@@ -0,0 +1 @@
|
|||||||
|
*.json text diff
|
||||||
@@ -55,6 +55,7 @@ body:
|
|||||||
label: Hotkey Configuration
|
label: Hotkey Configuration
|
||||||
description: >
|
description: >
|
||||||
Please provide your whkdrc or komorebi.ahk hotkey configuration file
|
Please provide your whkdrc or komorebi.ahk hotkey configuration file
|
||||||
|
render: shell
|
||||||
- type: textarea
|
- type: textarea
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -62,3 +63,4 @@ body:
|
|||||||
label: Output of komorebic check
|
label: Output of komorebic check
|
||||||
description: >
|
description: >
|
||||||
Please provide the output of `komorebic check`
|
Please provide the output of `komorebic check`
|
||||||
|
render: shell
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check and close feature issues
|
- name: Check and close feature issues
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const issue = context.payload.issue;
|
const issue = context.payload.issue;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
cargo-deny:
|
cargo-deny:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||||
@@ -43,10 +43,11 @@ jobs:
|
|||||||
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- run: rustup toolchain install stable --profile minimal
|
- run: rustup toolchain install stable --profile minimal
|
||||||
|
- run: rustup component add --toolchain stable-x86_64-pc-windows-msvc clippy
|
||||||
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
|
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
@@ -64,7 +65,7 @@ jobs:
|
|||||||
- run: |
|
- run: |
|
||||||
cargo install cargo-wix
|
cargo install cargo-wix
|
||||||
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
|
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
|
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
|
||||||
path: |
|
path: |
|
||||||
@@ -81,12 +82,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
- run: |
|
- 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
|
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
|
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||||
@@ -128,14 +129,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ github.event.release.tag_name }}
|
TAG=${{ github.event.release.tag_name }}
|
||||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
- run: |
|
- 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
|
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
|
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||||
@@ -170,14 +171,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ github.ref_name }}
|
TAG=${{ github.ref_name }}
|
||||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v8
|
||||||
- run: |
|
- 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
|
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
|
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||||
|
|||||||
@@ -6,3 +6,10 @@ dummy.go
|
|||||||
komorebic/applications.yaml
|
komorebic/applications.yaml
|
||||||
komorebic/applications.json
|
komorebic/applications.json
|
||||||
/.vs
|
/.vs
|
||||||
|
/bar-schema
|
||||||
|
/komorebi-schema
|
||||||
|
/.wrangler
|
||||||
|
/.xwin-cache
|
||||||
|
result
|
||||||
|
/.direnv
|
||||||
|
procdump.exe
|
||||||
|
|||||||
Generated
+2082
-1309
File diff suppressed because it is too large
Load Diff
+48
-44
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"komorebi",
|
"komorebi",
|
||||||
"komorebi-client",
|
"komorebi-client",
|
||||||
"komorebi-gui",
|
"komorebi-gui",
|
||||||
"komorebic",
|
"komorebi-layouts",
|
||||||
"komorebic-no-console",
|
"komorebic",
|
||||||
"komorebi-bar",
|
"komorebic-no-console",
|
||||||
"komorebi-themes",
|
"komorebi-bar",
|
||||||
"komorebi-shortcuts",
|
"komorebi-themes",
|
||||||
|
"komorebi-shortcuts",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@@ -19,61 +20,61 @@ chrono = "0.4"
|
|||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
crossbeam-utils = "0.8"
|
crossbeam-utils = "0.8"
|
||||||
color-eyre = "0.6"
|
color-eyre = "0.6"
|
||||||
eframe = "0.31"
|
eframe = "0.33"
|
||||||
egui_extras = "0.31"
|
egui_extras = "0.33"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
dunce = "1"
|
dunce = "1"
|
||||||
hotwatch = "0.5"
|
hotwatch = "0.5"
|
||||||
schemars = "0.8"
|
schemars = "1.1"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
strum = { version = "0.27", features = ["derive"] }
|
strum = { version = "0.28", features = ["derive"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-appender = "0.2"
|
tracing-appender = "0.2"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
paste = "1"
|
paste = "1"
|
||||||
sysinfo = "0.34"
|
sysinfo = "0.38"
|
||||||
uds_windows = "1"
|
uds_windows = "1"
|
||||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "a28c6559a9de2f92c142a714947a9b081776caca" }
|
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
|
||||||
windows-numerics = { version = "0.2" }
|
windows-numerics = { version = "0.3" }
|
||||||
windows-implement = { version = "0.60" }
|
windows-implement = { version = "0.60" }
|
||||||
windows-interface = { version = "0.59" }
|
windows-interface = { version = "0.59" }
|
||||||
windows-core = { version = "0.61" }
|
windows-core = { version = "0.62" }
|
||||||
shadow-rs = "1"
|
shadow-rs = "1"
|
||||||
which = "7"
|
which = "8"
|
||||||
|
|
||||||
[workspace.dependencies.windows]
|
[workspace.dependencies.windows]
|
||||||
version = "0.61"
|
version = "0.62"
|
||||||
features = [
|
features = [
|
||||||
"Foundation_Numerics",
|
"Foundation_Numerics",
|
||||||
"Win32_Devices",
|
"Win32_Devices",
|
||||||
"Win32_Devices_Display",
|
"Win32_Devices_Display",
|
||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
"Win32_UI_Shell_Common", # for IObjectArray
|
"Win32_UI_Shell_Common", # for IObjectArray
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_Globalization",
|
"Win32_Globalization",
|
||||||
"Win32_Graphics_Dwm",
|
"Win32_Graphics_Dwm",
|
||||||
"Win32_Graphics_Gdi",
|
"Win32_Graphics_Gdi",
|
||||||
"Win32_Graphics_Direct2D",
|
"Win32_Graphics_Direct2D",
|
||||||
"Win32_Graphics_Direct2D_Common",
|
"Win32_Graphics_Direct2D_Common",
|
||||||
"Win32_Graphics_Dxgi_Common",
|
"Win32_Graphics_Dxgi_Common",
|
||||||
"Win32_System_LibraryLoader",
|
"Win32_System_LibraryLoader",
|
||||||
"Win32_System_Power",
|
"Win32_System_Power",
|
||||||
"Win32_System_RemoteDesktop",
|
"Win32_System_RemoteDesktop",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
"Win32_UI_Accessibility",
|
"Win32_UI_Accessibility",
|
||||||
"Win32_UI_HiDpi",
|
"Win32_UI_HiDpi",
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_UI_Shell",
|
"Win32_UI_Shell",
|
||||||
"Win32_UI_Shell_Common",
|
"Win32_UI_Shell_Common",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
"Win32_System_SystemServices",
|
"Win32_System_SystemServices",
|
||||||
"Win32_System_WindowsProgramming",
|
"Win32_System_WindowsProgramming",
|
||||||
"Media",
|
"Media",
|
||||||
"Media_Control",
|
"Media_Control",
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.release-opt]
|
[profile.release-opt]
|
||||||
@@ -82,3 +83,6 @@ lto = true
|
|||||||
panic = "abort"
|
panic = "abort"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
strip = true
|
strip = true
|
||||||
|
|
||||||
|
[workspace.metadata.crane]
|
||||||
|
name = "komorebi-workspace"
|
||||||
|
|||||||
@@ -29,6 +29,36 @@ Tiling Window Management for Windows.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Note: Students using devices enrolled in mobile device management (MDM)
|
||||||
|
|
||||||
|
Your usage still falls under the [Komorebi License 2.0.0](./LICENSE.md).
|
||||||
|
|
||||||
|
You can email me at the address I sign my commits with (add `.patch` to the end
|
||||||
|
of any commit URL on GitHub to find it) from the address associated with your
|
||||||
|
institution with the subject "komorebi - student with an MDM device", and I will
|
||||||
|
be able to remove the splash intended for corporate users, whose usage falls
|
||||||
|
under the [Individual Commercial Use
|
||||||
|
License](https://lgug2z.com/software/komorebi).
|
||||||
|
|
||||||
|
This is currently a manual process - most days this shouldn't take more than
|
||||||
|
12h, and you will receive an email reply from me when the process is complete.
|
||||||
|
|
||||||
|
If you haven't had a reply to your email within 24h you can reach out to me on
|
||||||
|
Discord.
|
||||||
|
|
||||||
|
## Note: Unexpected mobile device management (MDM) detection prompts
|
||||||
|
|
||||||
|
You have most likely unintentionally enrolled your device in "Bring Your Own
|
||||||
|
Device" (BYOD) MDM. You can confirm if this is the case by running `dsregcmd
|
||||||
|
/status` and then take the appropriate steps to remove the MDM profile and take
|
||||||
|
back full control of your system.
|
||||||
|
|
||||||
|
If you need help doing this you can ask on Discord.
|
||||||
|
|
||||||
|
## Note: komorebi for Mac
|
||||||
|
|
||||||
|
komorebi for Mac lives [here](https://github.com/LGUG2Z/komorebi-for-mac) :)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
|
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
|
||||||
@@ -49,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
|
to [install](https://lgug2z.github.io/komorebi/installation.html) and
|
||||||
[configure](https://lgug2z.github.io/komorebi/example-configurations.html)
|
[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
|
_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).
|
complete [CLI reference](https://lgug2z.github.io/komorebi/cli/quickstart.html).
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
@@ -394,7 +424,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
|||||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.38"}
|
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi" }
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use komorebi_client::Notification;
|
use komorebi_client::Notification;
|
||||||
|
|||||||
@@ -0,0 +1,432 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Check schema.json and schema.bar.json for missing docstrings and map them to Rust source files.
|
||||||
|
|
||||||
|
This script analyzes the generated JSON schemas and identifies:
|
||||||
|
1. Type definitions ($defs) missing top-level descriptions
|
||||||
|
2. Enum variants missing descriptions (in oneOf/anyOf)
|
||||||
|
3. Enum variants missing titles (object variants in oneOf/anyOf)
|
||||||
|
4. Struct properties missing descriptions
|
||||||
|
5. Top-level schema properties missing descriptions
|
||||||
|
|
||||||
|
For each missing docstring, it attempts to find the corresponding Rust source
|
||||||
|
file and line number where the docstring should be added.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MissingDoc:
|
||||||
|
type_name: str
|
||||||
|
kind: str # "type", "variant", "property", "variant_title"
|
||||||
|
item_name: Optional[str] # variant or property name
|
||||||
|
rust_file: Optional[str] = None
|
||||||
|
rust_line: Optional[int] = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
location = ""
|
||||||
|
if self.rust_file and self.rust_line:
|
||||||
|
location = f" -> {self.rust_file}:{self.rust_line}"
|
||||||
|
elif self.rust_file:
|
||||||
|
location = f" -> {self.rust_file}"
|
||||||
|
|
||||||
|
if self.kind == "type":
|
||||||
|
return f"[TYPE] {self.type_name}{location}"
|
||||||
|
elif self.kind == "variant":
|
||||||
|
return f"[VARIANT] {self.type_name}::{self.item_name}{location}"
|
||||||
|
elif self.kind == "variant_title":
|
||||||
|
return f"[VARIANT_TITLE] {self.type_name}::{self.item_name}{location}"
|
||||||
|
else:
|
||||||
|
return f"[PROPERTY] {self.type_name}.{self.item_name}{location}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SchemaConfig:
|
||||||
|
"""Configuration for a schema to check."""
|
||||||
|
|
||||||
|
schema_file: str
|
||||||
|
search_paths: list[str]
|
||||||
|
display_name: str
|
||||||
|
|
||||||
|
|
||||||
|
def find_rust_definition(
|
||||||
|
type_name: str, item_name: Optional[str], kind: str, search_paths: list[Path]
|
||||||
|
) -> tuple[Optional[str], Optional[int]]:
|
||||||
|
"""Find the Rust file and line number for a type/variant/property definition."""
|
||||||
|
|
||||||
|
if kind == "type":
|
||||||
|
patterns = [
|
||||||
|
rf"pub\s+enum\s+{type_name}\b",
|
||||||
|
rf"pub\s+struct\s+{type_name}\b",
|
||||||
|
]
|
||||||
|
elif kind in ("variant", "variant_title"):
|
||||||
|
patterns = [
|
||||||
|
rf"^\s*{re.escape(item_name)}\s*[,\(\{{]",
|
||||||
|
rf"^\s*{re.escape(item_name)}\s*$",
|
||||||
|
rf"^\s*#\[.*\]\s*\n\s*{re.escape(item_name)}\b",
|
||||||
|
]
|
||||||
|
else: # property
|
||||||
|
patterns = [rf"pub\s+{re.escape(item_name)}\s*:"]
|
||||||
|
|
||||||
|
for search_path in search_paths:
|
||||||
|
for rust_file in search_path.rglob("*.rs"):
|
||||||
|
try:
|
||||||
|
content = rust_file.read_text()
|
||||||
|
lines = content.split("\n")
|
||||||
|
|
||||||
|
if kind == "type":
|
||||||
|
for pattern in patterns:
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if re.search(pattern, line):
|
||||||
|
return str(rust_file), i + 1
|
||||||
|
|
||||||
|
elif kind in ("variant", "variant_title", "property"):
|
||||||
|
parent_pattern = rf"pub\s+(?:enum|struct)\s+{type_name}\b"
|
||||||
|
in_type = False
|
||||||
|
brace_count = 0
|
||||||
|
found_open_brace = False
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if re.search(parent_pattern, line):
|
||||||
|
in_type = True
|
||||||
|
brace_count = 0
|
||||||
|
found_open_brace = False
|
||||||
|
|
||||||
|
if in_type:
|
||||||
|
if "{" in line:
|
||||||
|
found_open_brace = True
|
||||||
|
brace_count += line.count("{") - line.count("}")
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
if re.search(pattern, line):
|
||||||
|
return str(rust_file), i + 1
|
||||||
|
|
||||||
|
if found_open_brace and brace_count <= 0:
|
||||||
|
in_type = False
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_variant_identifier(variant: dict) -> str:
|
||||||
|
"""Extract a meaningful identifier for a variant.
|
||||||
|
|
||||||
|
Tries to find the best identifier by checking:
|
||||||
|
1. A top-level const value (e.g., {"const": "Linear"})
|
||||||
|
2. A property with a const value (e.g., {"kind": {"const": "Bar"}})
|
||||||
|
3. The first required property name
|
||||||
|
4. The type field
|
||||||
|
5. Falls back to "unknown"
|
||||||
|
"""
|
||||||
|
# Check for top-level const value (simple enum variant)
|
||||||
|
if "const" in variant:
|
||||||
|
return str(variant["const"])
|
||||||
|
|
||||||
|
properties = variant.get("properties", {})
|
||||||
|
|
||||||
|
# Check for a property with a const value (common pattern for tagged enums)
|
||||||
|
for prop_name, prop_def in properties.items():
|
||||||
|
if isinstance(prop_def, dict) and "const" in prop_def:
|
||||||
|
return str(prop_def["const"])
|
||||||
|
|
||||||
|
# Fall back to first required property name
|
||||||
|
required = variant.get("required", [])
|
||||||
|
if required:
|
||||||
|
return str(required[0])
|
||||||
|
|
||||||
|
# Fall back to type
|
||||||
|
if "type" in variant:
|
||||||
|
return str(variant["type"])
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def check_type_description(type_name: str, type_def: dict) -> list[MissingDoc]:
|
||||||
|
"""Check if a type definition has proper documentation."""
|
||||||
|
missing = []
|
||||||
|
has_top_description = "description" in type_def
|
||||||
|
|
||||||
|
# Always check for top-level type description first
|
||||||
|
# (except for types that are purely references or have special handling)
|
||||||
|
needs_type_description = True
|
||||||
|
|
||||||
|
# Check oneOf variants (tagged enums with variant descriptions)
|
||||||
|
if "oneOf" in type_def:
|
||||||
|
# oneOf types should have a top-level description
|
||||||
|
if not has_top_description:
|
||||||
|
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||||
|
|
||||||
|
for variant in type_def["oneOf"]:
|
||||||
|
# Case 1: Simple const variant (e.g., {"const": "Swap", "description": "..."})
|
||||||
|
variant_name = variant.get("const") or variant.get("title")
|
||||||
|
if variant_name and "description" not in variant:
|
||||||
|
missing.append(
|
||||||
|
MissingDoc(type_name, "variant", str(variant_name), None, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Case 2: String enum inside oneOf (e.g., {"type": "string", "enum": [...]})
|
||||||
|
# These variants don't have individual descriptions in the schema
|
||||||
|
if "enum" in variant and variant.get("type") == "string":
|
||||||
|
for enum_variant in variant["enum"]:
|
||||||
|
missing.append(
|
||||||
|
MissingDoc(type_name, "variant", str(enum_variant), None, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Case 3: Object variant with properties (e.g., CubicBezier)
|
||||||
|
if "properties" in variant and "description" not in variant:
|
||||||
|
for prop_name in variant.get("required", []):
|
||||||
|
missing.append(
|
||||||
|
MissingDoc(type_name, "variant", str(prop_name), None, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Case 4: Object variant missing title (needed for schema UI display)
|
||||||
|
# Object variants should have a title or const for proper display in editors
|
||||||
|
if (
|
||||||
|
"properties" in variant
|
||||||
|
and "title" not in variant
|
||||||
|
and "const" not in variant
|
||||||
|
):
|
||||||
|
# Try to find a good identifier for the variant (for display only)
|
||||||
|
variant_id = _get_variant_identifier(variant)
|
||||||
|
missing.append(
|
||||||
|
MissingDoc(type_name, "variant_title", str(variant_id), None, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check anyOf variants - check each variant individually
|
||||||
|
elif "anyOf" in type_def:
|
||||||
|
# anyOf types should have a top-level description
|
||||||
|
if not has_top_description:
|
||||||
|
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||||
|
|
||||||
|
# Check each variant for description (skip pure $ref and null types)
|
||||||
|
for variant in type_def["anyOf"]:
|
||||||
|
# Skip null type variants (used for Option<T>)
|
||||||
|
if variant.get("type") == "null":
|
||||||
|
continue
|
||||||
|
# Skip pure $ref variants (the referenced type is checked separately)
|
||||||
|
if "$ref" in variant and len(variant) == 1:
|
||||||
|
continue
|
||||||
|
# Skip $ref variants that have a description
|
||||||
|
if "$ref" in variant and "description" in variant:
|
||||||
|
continue
|
||||||
|
# Variant with $ref but no description
|
||||||
|
if "$ref" in variant and "description" not in variant:
|
||||||
|
# Extract the type name from the $ref
|
||||||
|
ref_name = variant["$ref"].split("/")[-1]
|
||||||
|
missing.append(MissingDoc(type_name, "variant", ref_name, None, None))
|
||||||
|
# Non-ref variant without description
|
||||||
|
elif "description" not in variant and "$ref" not in variant:
|
||||||
|
# Try to identify the variant by its type or const
|
||||||
|
variant_id = variant.get("const") or variant.get("type") or "unknown"
|
||||||
|
missing.append(
|
||||||
|
MissingDoc(type_name, "variant", str(variant_id), None, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for missing title on object variants in anyOf
|
||||||
|
if (
|
||||||
|
"properties" in variant
|
||||||
|
and "title" not in variant
|
||||||
|
and "const" not in variant
|
||||||
|
):
|
||||||
|
variant_id = _get_variant_identifier(variant)
|
||||||
|
missing.append(
|
||||||
|
MissingDoc(type_name, "variant_title", str(variant_id), None, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check simple string enums (no oneOf means no variant descriptions possible in schema)
|
||||||
|
elif "enum" in type_def:
|
||||||
|
if not has_top_description:
|
||||||
|
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||||
|
# Each enum variant needs a docstring - these can't have descriptions in simple enum format
|
||||||
|
for variant in type_def["enum"]:
|
||||||
|
missing.append(MissingDoc(type_name, "variant", str(variant), None, None))
|
||||||
|
|
||||||
|
# Check struct properties
|
||||||
|
elif "properties" in type_def:
|
||||||
|
# Structs should always have a top-level description
|
||||||
|
if not has_top_description:
|
||||||
|
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||||
|
|
||||||
|
for prop_name, prop_def in type_def["properties"].items():
|
||||||
|
if "description" not in prop_def:
|
||||||
|
missing.append(MissingDoc(type_name, "property", prop_name, None, None))
|
||||||
|
|
||||||
|
# Simple type without description (like PathBuf, Hex)
|
||||||
|
elif not has_top_description:
|
||||||
|
# Only flag if it has a concrete type (not just a $ref)
|
||||||
|
if type_def.get("type") is not None:
|
||||||
|
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||||
|
|
||||||
|
return missing
|
||||||
|
|
||||||
|
|
||||||
|
def check_top_level_properties(schema: dict, root_type_name: str) -> list[MissingDoc]:
|
||||||
|
"""Check top-level schema properties for missing descriptions."""
|
||||||
|
missing = []
|
||||||
|
properties = schema.get("properties", {})
|
||||||
|
|
||||||
|
for prop_name, prop_def in properties.items():
|
||||||
|
if "description" not in prop_def:
|
||||||
|
missing.append(
|
||||||
|
MissingDoc(root_type_name, "property", prop_name, None, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
return missing
|
||||||
|
|
||||||
|
|
||||||
|
def check_schema(
|
||||||
|
schema_path: Path,
|
||||||
|
search_paths: list[Path],
|
||||||
|
project_root: Path,
|
||||||
|
display_name: str,
|
||||||
|
) -> tuple[list[MissingDoc], int]:
|
||||||
|
"""Check a single schema file and return missing docs and exit code."""
|
||||||
|
if not schema_path.exists():
|
||||||
|
print(f"Error: {schema_path.name} not found at {schema_path}")
|
||||||
|
return [], 1
|
||||||
|
|
||||||
|
with open(schema_path) as f:
|
||||||
|
schema = json.load(f)
|
||||||
|
|
||||||
|
all_missing: list[MissingDoc] = []
|
||||||
|
|
||||||
|
# Check top-level schema properties
|
||||||
|
root_type_name = schema.get("title", "Root")
|
||||||
|
all_missing.extend(check_top_level_properties(schema, root_type_name))
|
||||||
|
|
||||||
|
# Check all type definitions
|
||||||
|
for type_name, type_def in sorted(schema.get("$defs", {}).items()):
|
||||||
|
# Skip PerAnimationPrefixConfig2/3 as they're generated variants
|
||||||
|
if (
|
||||||
|
type_name.startswith("PerAnimationPrefixConfig")
|
||||||
|
and type_name != "PerAnimationPrefixConfig"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
all_missing.extend(check_type_description(type_name, type_def))
|
||||||
|
|
||||||
|
# Find Rust source locations
|
||||||
|
print(f"Scanning Rust source files for {display_name}...", file=sys.stderr)
|
||||||
|
for doc in all_missing:
|
||||||
|
doc.rust_file, doc.rust_line = find_rust_definition(
|
||||||
|
doc.type_name, doc.item_name, doc.kind, search_paths
|
||||||
|
)
|
||||||
|
if doc.rust_file:
|
||||||
|
try:
|
||||||
|
doc.rust_file = str(Path(doc.rust_file).relative_to(project_root))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return all_missing, 0
|
||||||
|
|
||||||
|
|
||||||
|
def print_results(all_missing: list[MissingDoc], display_name: str) -> None:
|
||||||
|
"""Print the results for a schema check."""
|
||||||
|
# Group by file
|
||||||
|
by_file: dict[str, list[MissingDoc]] = {}
|
||||||
|
external: list[MissingDoc] = []
|
||||||
|
|
||||||
|
for doc in all_missing:
|
||||||
|
if doc.rust_file:
|
||||||
|
by_file.setdefault(doc.rust_file, []).append(doc)
|
||||||
|
else:
|
||||||
|
external.append(doc)
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(f"MISSING DOCSTRINGS IN SCHEMA ({display_name})")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
type_count = sum(1 for d in all_missing if d.kind == "type")
|
||||||
|
variant_count = sum(1 for d in all_missing if d.kind == "variant")
|
||||||
|
variant_title_count = sum(1 for d in all_missing if d.kind == "variant_title")
|
||||||
|
prop_count = sum(1 for d in all_missing if d.kind == "property")
|
||||||
|
|
||||||
|
print(f"\nTotal: {len(all_missing)} missing docstrings/titles")
|
||||||
|
print(f" - {type_count} types")
|
||||||
|
print(f" - {variant_count} variants")
|
||||||
|
print(f" - {variant_title_count} variant titles")
|
||||||
|
print(f" - {prop_count} properties")
|
||||||
|
|
||||||
|
# Print by file
|
||||||
|
for rust_file in sorted(by_file.keys()):
|
||||||
|
docs = sorted(by_file[rust_file], key=lambda d: d.rust_line or 0)
|
||||||
|
print(f"\n{rust_file}:")
|
||||||
|
print("-" * len(rust_file))
|
||||||
|
for doc in docs:
|
||||||
|
print(f" {doc}")
|
||||||
|
|
||||||
|
# Print external items (types not found in source)
|
||||||
|
if external:
|
||||||
|
print(f"\nExternal/Unknown location:")
|
||||||
|
print("-" * 25)
|
||||||
|
for doc in external:
|
||||||
|
print(f" {doc}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
project_root = Path.cwd()
|
||||||
|
|
||||||
|
# Define schemas to check with their respective search paths
|
||||||
|
schemas = [
|
||||||
|
SchemaConfig(
|
||||||
|
schema_file="schema.json",
|
||||||
|
search_paths=["komorebi/src", "komorebi-themes/src"],
|
||||||
|
display_name="komorebi",
|
||||||
|
),
|
||||||
|
SchemaConfig(
|
||||||
|
schema_file="schema.bar.json",
|
||||||
|
search_paths=["komorebi-bar/src", "komorebi-themes/src"],
|
||||||
|
display_name="komorebi-bar",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
total_missing = 0
|
||||||
|
has_errors = False
|
||||||
|
|
||||||
|
for schema_config in schemas:
|
||||||
|
schema_path = project_root / schema_config.schema_file
|
||||||
|
search_paths = [
|
||||||
|
project_root / p
|
||||||
|
for p in schema_config.search_paths
|
||||||
|
if (project_root / p).exists()
|
||||||
|
]
|
||||||
|
|
||||||
|
missing, error_code = check_schema(
|
||||||
|
schema_path,
|
||||||
|
search_paths,
|
||||||
|
project_root,
|
||||||
|
schema_config.display_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if error_code != 0:
|
||||||
|
has_errors = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
print_results(missing, schema_config.display_name)
|
||||||
|
total_missing += len(missing)
|
||||||
|
|
||||||
|
# Print combined summary
|
||||||
|
if len(schemas) > 1:
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("COMBINED SUMMARY")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"Total missing docstrings across all schemas: {total_missing}")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
if has_errors:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 1 if total_missing > 0 else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
[graph]
|
[graph]
|
||||||
targets = [
|
targets = [
|
||||||
"x86_64-pc-windows-msvc",
|
"x86_64-pc-windows-msvc",
|
||||||
"i686-pc-windows-msvc",
|
"i686-pc-windows-msvc",
|
||||||
"aarch64-pc-windows-msvc",
|
"aarch64-pc-windows-msvc",
|
||||||
]
|
]
|
||||||
all-features = false
|
all-features = false
|
||||||
no-default-features = false
|
no-default-features = false
|
||||||
@@ -12,30 +12,31 @@ feature-depth = 1
|
|||||||
|
|
||||||
[advisories]
|
[advisories]
|
||||||
ignore = [
|
ignore = [
|
||||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
||||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
|
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
|
||||||
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" }
|
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[licenses]
|
[licenses]
|
||||||
allow = [
|
allow = [
|
||||||
"0BSD",
|
"0BSD",
|
||||||
"Apache-2.0",
|
"Apache-2.0",
|
||||||
"Artistic-2.0",
|
"Apache-2.0 WITH LLVM-exception",
|
||||||
"BSD-2-Clause",
|
"Artistic-2.0",
|
||||||
"BSD-3-Clause",
|
"BSD-2-Clause",
|
||||||
"BSL-1.0",
|
"BSD-3-Clause",
|
||||||
"CC0-1.0",
|
"BSL-1.0",
|
||||||
"ISC",
|
"CC0-1.0",
|
||||||
"MIT",
|
"ISC",
|
||||||
"MIT-0",
|
"MIT",
|
||||||
"MPL-2.0",
|
"MIT-0",
|
||||||
"OFL-1.1",
|
"MPL-2.0",
|
||||||
"Ubuntu-font-1.0",
|
"OFL-1.1",
|
||||||
"Unicode-3.0",
|
"Ubuntu-font-1.0",
|
||||||
"Zlib",
|
"Unicode-3.0",
|
||||||
"LicenseRef-Komorebi-2.0"
|
"Zlib",
|
||||||
|
"LicenseRef-Komorebi-2.0",
|
||||||
]
|
]
|
||||||
confidence-threshold = 0.8
|
confidence-threshold = 0.8
|
||||||
|
|
||||||
@@ -49,6 +50,11 @@ crate = "komorebi-client"
|
|||||||
expression = "LicenseRef-Komorebi-2.0"
|
expression = "LicenseRef-Komorebi-2.0"
|
||||||
license-files = []
|
license-files = []
|
||||||
|
|
||||||
|
[[licenses.clarify]]
|
||||||
|
crate = "komorebi-layouts"
|
||||||
|
expression = "LicenseRef-Komorebi-2.0"
|
||||||
|
license-files = []
|
||||||
|
|
||||||
[[licenses.clarify]]
|
[[licenses.clarify]]
|
||||||
crate = "komorebic"
|
crate = "komorebic"
|
||||||
expression = "LicenseRef-Komorebi-2.0"
|
expression = "LicenseRef-Komorebi-2.0"
|
||||||
@@ -94,6 +100,11 @@ crate = "base16-egui-themes"
|
|||||||
expression = "MIT"
|
expression = "MIT"
|
||||||
license-files = []
|
license-files = []
|
||||||
|
|
||||||
|
[[licenses.clarify]]
|
||||||
|
crate = "win32-display-data"
|
||||||
|
expression = "0BSD"
|
||||||
|
license-files = []
|
||||||
|
|
||||||
[bans]
|
[bans]
|
||||||
multiple-versions = "allow"
|
multiple-versions = "allow"
|
||||||
wildcards = "allow"
|
wildcards = "allow"
|
||||||
@@ -106,11 +117,12 @@ unknown-registry = "deny"
|
|||||||
unknown-git = "deny"
|
unknown-git = "deny"
|
||||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||||
allow-git = [
|
allow-git = [
|
||||||
"https://github.com/LGUG2Z/base16-egui-themes",
|
"https://github.com/LGUG2Z/base16-egui-themes",
|
||||||
"https://github.com/LGUG2Z/catppuccin-egui",
|
"https://github.com/LGUG2Z/windows-icons",
|
||||||
"https://github.com/LGUG2Z/windows-icons",
|
"https://github.com/LGUG2Z/win32-display-data",
|
||||||
"https://github.com/LGUG2Z/win32-display-data",
|
"https://github.com/LGUG2Z/flavours",
|
||||||
"https://github.com/LGUG2Z/flavours",
|
"https://github.com/LGUG2Z/base16_color_scheme",
|
||||||
"https://github.com/LGUG2Z/base16_color_scheme",
|
"https://github.com/LGUG2Z/whkd",
|
||||||
"https://github.com/LGUG2Z/whkd",
|
"https://github.com/LGUG2Z/catppuccin-egui",
|
||||||
|
"https://github.com/amPerl/egui-phosphor",
|
||||||
]
|
]
|
||||||
|
|||||||
+416
-351
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
@@ -10,8 +10,8 @@ Options:
|
|||||||
Desired ease function for animation
|
Desired ease function for animation
|
||||||
|
|
||||||
[default: linear]
|
[default: linear]
|
||||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint,
|
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo,
|
||||||
ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||||
|
|
||||||
-a, --animation-type <ANIMATION_TYPE>
|
-a, --animation-type <ANIMATION_TYPE>
|
||||||
Animation type to apply the style to. If not specified, sets global style
|
Animation type to apply the style to. If not specified, sets global style
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# cancel-preselect
|
||||||
|
|
||||||
|
```
|
||||||
|
Cancel a workspace preselect set by the preselect-direction command, if one exists
|
||||||
|
|
||||||
|
Usage: komorebic.exe cancel-preselect
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help
|
||||||
|
Print help
|
||||||
|
|
||||||
|
```
|
||||||
@@ -12,9 +12,6 @@ Options:
|
|||||||
--whkd
|
--whkd
|
||||||
Enable autostart of whkd
|
Enable autostart of whkd
|
||||||
|
|
||||||
--ahk
|
|
||||||
Enable autostart of ahk
|
|
||||||
|
|
||||||
--bar
|
--bar
|
||||||
Enable autostart of komorebi-bar
|
Enable autostart of komorebi-bar
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ Options:
|
|||||||
--whkd
|
--whkd
|
||||||
Kill whkd if it is running as a background process
|
Kill whkd if it is running as a background process
|
||||||
|
|
||||||
--ahk
|
|
||||||
Kill ahk if it is running as a background process
|
|
||||||
|
|
||||||
--bar
|
--bar
|
||||||
Kill komorebi-bar if it is running as a background process
|
Kill komorebi-bar if it is running as a background process
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# license
|
||||||
|
|
||||||
|
```
|
||||||
|
Specify an email associated with an Individual Commercial Use License
|
||||||
|
|
||||||
|
Usage: komorebic.exe license <EMAIL>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<EMAIL>
|
||||||
|
Email address associated with an Individual Commercial Use License
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help
|
||||||
|
Print help
|
||||||
|
|
||||||
|
```
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# preselect-direction
|
||||||
|
|
||||||
|
```
|
||||||
|
Preselect the specified direction for the next window to be spawned on supported layouts
|
||||||
|
|
||||||
|
Usage: komorebic.exe preselect-direction <OPERATION_DIRECTION>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<OPERATION_DIRECTION>
|
||||||
|
[possible values: left, right, up, down]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help
|
||||||
|
Print help
|
||||||
|
|
||||||
|
```
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# promote-swap
|
||||||
|
|
||||||
|
```
|
||||||
|
Promote the focused window to the largest tile by swapping container indices with the largest tile
|
||||||
|
|
||||||
|
Usage: komorebic.exe promote-swap
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help
|
||||||
|
Print help
|
||||||
|
|
||||||
|
```
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# promote
|
# promote
|
||||||
|
|
||||||
```
|
```
|
||||||
Promote the focused window to the top of the tree
|
Promote the focused window to the largest tile via container removal and re-insertion
|
||||||
|
|
||||||
Usage: komorebic.exe promote
|
Usage: komorebic.exe promote
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ Options:
|
|||||||
--whkd
|
--whkd
|
||||||
Start whkd in a background process
|
Start whkd in a background process
|
||||||
|
|
||||||
--ahk
|
|
||||||
Start autohotkey configuration file
|
|
||||||
|
|
||||||
--bar
|
--bar
|
||||||
Start komorebi-bar in a background process
|
Start komorebi-bar in a background process
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ Options:
|
|||||||
--whkd
|
--whkd
|
||||||
Stop whkd if it is running as a background process
|
Stop whkd if it is running as a background process
|
||||||
|
|
||||||
--ahk
|
|
||||||
Stop ahk if it is running as a background process
|
|
||||||
|
|
||||||
--bar
|
--bar
|
||||||
Stop komorebi-bar if it is running as a background process
|
Stop komorebi-bar if it is running as a background process
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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).
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
# Layout Ratios
|
||||||
|
|
||||||
|
With `komorebi` you can customize the split ratios for various layouts using
|
||||||
|
`column_ratios` and `row_ratios` in the `layout_options` configuration.
|
||||||
|
|
||||||
|
## Before and After
|
||||||
|
|
||||||
|
BSP layout example:
|
||||||
|
|
||||||
|
**Before** (default 50/50 splits):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**After** (with `column_ratios: [0.7]` and `row_ratios: [0.6]`):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"monitors": [
|
||||||
|
{
|
||||||
|
"workspaces": [
|
||||||
|
{
|
||||||
|
"name": "main",
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.3, 0.4],
|
||||||
|
"row_ratios": [0.4, 0.3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can specify up to 5 ratio values (defined by `MAX_RATIOS` constant). Each value should be between 0.1 and 0.9
|
||||||
|
(defined by `MIN_RATIO` and `MAX_RATIO` constants). Values outside this range are automatically clamped.
|
||||||
|
Columns or rows without a specified ratio will share the remaining space equally.
|
||||||
|
|
||||||
|
## Usage by Layout
|
||||||
|
|
||||||
|
| Layout | `column_ratios` | `row_ratios` |
|
||||||
|
|--------|-----------------|--------------|
|
||||||
|
| **Columns** | Width of each column | - |
|
||||||
|
| **Rows** | - | Height of each row |
|
||||||
|
| **Grid** | Width of each column (rows are equal height) | - |
|
||||||
|
| **BSP** | `[0]` as horizontal split ratio | `[0]` as vertical split ratio |
|
||||||
|
| **VerticalStack** | `[0]` as primary column width | Stack row heights |
|
||||||
|
| **RightMainVerticalStack** | `[0]` as primary column width | Stack row heights |
|
||||||
|
| **HorizontalStack** | Stack column widths | `[0]` as primary row height |
|
||||||
|
| **UltrawideVerticalStack** | `[0]` center, `[1]` left column | Tertiary stack row heights |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Columns Layout with Custom Widths
|
||||||
|
|
||||||
|
Create 3 columns with 30%, 40%, and 30% widths:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.3, 0.4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The third column automatically gets the remaining 30%.
|
||||||
|
|
||||||
|
### Rows Layout with Custom Heights
|
||||||
|
|
||||||
|
Create 3 rows with 20%, 50%, and 30% heights:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"row_ratios": [0.2, 0.5]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The third row automatically gets the remaining 30%.
|
||||||
|
|
||||||
|
### Grid Layout with Custom Column Widths
|
||||||
|
|
||||||
|
Grid with custom column widths (rows within each column are always equal height):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.4, 0.6]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The Grid layout only supports `column_ratios`. Rows within each column are always
|
||||||
|
divided equally because the number of rows per column varies dynamically based on window count.
|
||||||
|
|
||||||
|
### VerticalStack with Custom Ratios
|
||||||
|
|
||||||
|
Primary column takes 60% width, and the stack rows are split 30%/70%:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.6],
|
||||||
|
"row_ratios": [0.3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The second row automatically gets the remaining 70%.
|
||||||
|
|
||||||
|
### HorizontalStack with Custom Ratios
|
||||||
|
|
||||||
|
Primary row takes 70% height, and the stack columns are split 40%/60%:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"row_ratios": [0.7],
|
||||||
|
"column_ratios": [0.4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The second column automatically gets the remaining 60%.
|
||||||
|
|
||||||
|
### UltrawideVerticalStack with Custom Ratios
|
||||||
|
|
||||||
|
Center column at 50%, left column at 25% (remaining 25% goes to tertiary stack),
|
||||||
|
with tertiary rows split 40%/60%:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.5, 0.25],
|
||||||
|
"row_ratios": [0.4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The second row automatically gets the remaining 60%.
|
||||||
|
|
||||||
|
### BSP Layout with Custom Split Ratios
|
||||||
|
|
||||||
|
Use separate ratios for horizontal (left/right) and vertical (top/bottom) splits:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layout_options": {
|
||||||
|
"column_ratios": [0.6],
|
||||||
|
"row_ratios": [0.3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `column_ratios[0]`: Controls all horizontal splits (left window gets 60%, right gets 40%)
|
||||||
|
- `row_ratios[0]`: Controls all vertical splits (top window gets 30%, bottom gets 70%)
|
||||||
|
|
||||||
|
Note: BSP only uses the first value (`[0]`) from each ratio array. This single ratio is applied
|
||||||
|
consistently to all splits of that type throughout the layout. Additional values in the arrays are ignored.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Ratios are clamped between 0.1 and 0.9 (prevents zero-sized windows and ensures space for other windows)
|
||||||
|
- Default ratio is 0.5 (50%) when not specified, except for UltrawideVerticalStack secondary column which defaults to 0.25 (25%)
|
||||||
|
- Ratios are applied **progressively** - a ratio is only used when there are more windows to place after the current one
|
||||||
|
- The **last window always takes the remaining space**, regardless of defined ratios
|
||||||
|
- **Ratios that would sum to 100% or more are automatically truncated** at config load time to ensure there's always space for additional windows
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## Progressive Ratio Behavior
|
||||||
|
|
||||||
|
Ratios are applied progressively as windows are added. For example, with `row_ratios: [0.3, 0.5]` in a VerticalStack:
|
||||||
|
|
||||||
|
| Windows in Stack | Row Heights |
|
||||||
|
|------------------|-------------|
|
||||||
|
| 1 | 100% |
|
||||||
|
| 2 | 30%, 70% (remainder) |
|
||||||
|
| 3 | 30%, 50%, 20% (remainder) |
|
||||||
|
| 4 | 30%, 50%, 10%, 10% (remainder split equally) |
|
||||||
|
| 5 | 30%, 50%, 6.67%, 6.67%, 6.67% |
|
||||||
|
|
||||||
|
## Automatic Ratio Truncation
|
||||||
|
|
||||||
|
When ratios sum to 100% (or more), they are automatically truncated at config load time.
|
||||||
|
|
||||||
|
For example, if you configure `column_ratios: [0.4, 0.3, 0.3]` (sums to 100%), the last ratio (0.3) is automatically removed, resulting in effectively `[0.4, 0.3]`. This ensures there's always remaining space for the last window.
|
||||||
|
|
||||||
|
| Configured Ratios | Effective Ratios | Reason |
|
||||||
|
|-------------------|------------------|--------|
|
||||||
|
| `[0.3, 0.4]` | `[0.3, 0.4]` | Sum is 0.7, below 1.0 |
|
||||||
|
| `[0.4, 0.3, 0.3]` | `[0.4, 0.3]` | Sum would be 1.0, last ratio truncated |
|
||||||
|
| `[0.5, 0.5]` | `[0.5]` | Sum would be 1.0, last ratio truncated |
|
||||||
|
| `[0.6, 0.5]` | `[0.6]` | Sum would exceed 1.0, last ratio truncated |
|
||||||
|
|
||||||
|
This ensures the layout always fills 100% of the available space and new windows are never placed outside the visible area.
|
||||||
@@ -16,6 +16,19 @@ the example files have been downloaded. For most new users this will be in the
|
|||||||
komorebic quickstart
|
komorebic quickstart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Corporate Devices Enrolled in MDM
|
||||||
|
|
||||||
|
If you are using `komorebi` on a corporate device enrolled in mobile device
|
||||||
|
management, you will receive a pop-up when you run `komorebic start` reminding
|
||||||
|
you that the [Komorebi License](https://github.com/LGUG2Z/komorebi-license) does
|
||||||
|
not permit any kind of commercial use.
|
||||||
|
|
||||||
|
You can remove this pop-up by running `komorebic license <email>` with the email
|
||||||
|
associated with your Individual Commercial Use License. A single HTTP request
|
||||||
|
will be sent with the given email address to verify license validity.
|
||||||
|
|
||||||
|
## Starting komorebi
|
||||||
|
|
||||||
With the example configurations downloaded, you can now start `komorebi`,
|
With the example configurations downloaded, you can now start `komorebi`,
|
||||||
`komorebi-bar` and `whkd`.
|
`komorebi-bar` and `whkd`.
|
||||||
|
|
||||||
@@ -23,6 +36,9 @@ With the example configurations downloaded, you can now start `komorebi`,
|
|||||||
komorebic start --whkd --bar
|
komorebic start --whkd --bar
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you don't want to use the komorebi status bar, you can remove the `--bar` option
|
||||||
|
from the above command.
|
||||||
|
|
||||||
## komorebi.json
|
## komorebi.json
|
||||||
|
|
||||||
The example window manager configuration sets some sane defaults and provides
|
The example window manager configuration sets some sane defaults and provides
|
||||||
@@ -67,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
|
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.
|
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
|
you should be bring up with the billion dollar company and not with me, the
|
||||||
solo developer.
|
solo developer.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.38/schema.bar.json",
|
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.41/schema.bar.json",
|
||||||
"monitor": 0,
|
|
||||||
"font_family": "JetBrains Mono",
|
"font_family": "JetBrains Mono",
|
||||||
"theme": {
|
"theme": {
|
||||||
"palette": "Base16",
|
"palette": "Base16",
|
||||||
@@ -32,24 +31,24 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Media": {
|
"Media": {
|
||||||
"enable": true
|
"enable": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Storage": {
|
"Storage": {
|
||||||
"enable": true
|
"enable": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Memory": {
|
"Memory": {
|
||||||
"enable": true
|
"enable": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Network": {
|
"Network": {
|
||||||
"enable": true,
|
"enable": false,
|
||||||
"show_total_data_transmitted": true,
|
"show_activity": true,
|
||||||
"show_network_activity": true
|
"show_total_activity": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.38/schema.json",
|
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.41/schema.json",
|
||||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||||
"window_hiding_behaviour": "Cloak",
|
"window_hiding_behaviour": "Cloak",
|
||||||
"cross_monitor_move_behaviour": "Insert",
|
"cross_monitor_move_behaviour": "Insert",
|
||||||
|
|||||||
Generated
+179
@@ -0,0 +1,179 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1766774972,
|
||||||
|
"narHash": "sha256-8qxEFpj4dVmIuPn9j9z6NTbU+hrcGjBOvaxTzre5HmM=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "01bc1d404a51a0a07e9d8759cd50a7903e218c82",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1761588595,
|
||||||
|
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765835352,
|
||||||
|
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"git-hooks-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765911976,
|
||||||
|
"narHash": "sha256-t3T/xm8zstHRLx+pIHxVpQTiySbKqcQbK+r+01XVKc0=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"git-hooks-nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1766870016,
|
||||||
|
"narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-lib": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765674936,
|
||||||
|
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"git-hooks-nix": "git-hooks-nix",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay",
|
||||||
|
"treefmt-nix": "treefmt-nix"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1766890375,
|
||||||
|
"narHash": "sha256-0Zi7ChAtjq/efwQYmp7kOJPcSt6ya9ynSUe6ppgZhsQ=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "91e1f7a0017065360f447622d11b7ce6ed04772f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"treefmt-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1766000401,
|
||||||
|
"narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
{
|
||||||
|
description = "komorebi for Windows";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
|
crane.url = "github:ipetkov/crane";
|
||||||
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
|
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||||
|
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
git-hooks-nix.url = "github:cachix/git-hooks.nix";
|
||||||
|
git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs@{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-parts,
|
||||||
|
crane,
|
||||||
|
rust-overlay,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
windowsSdkVersion = "10.0.26100";
|
||||||
|
windowsCrtVersion = "14.44.17.14";
|
||||||
|
|
||||||
|
mkWindowsSdk =
|
||||||
|
pkgs:
|
||||||
|
pkgs.stdenvNoCC.mkDerivation {
|
||||||
|
name = "windows-sdk-${windowsSdkVersion}-crt-${windowsCrtVersion}";
|
||||||
|
|
||||||
|
nativeBuildInputs = [ pkgs.xwin ];
|
||||||
|
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHashMode = "recursive";
|
||||||
|
outputHash = "sha256-6cLS5q1BDRpLPScfmmKpTTEHUzsgKTKD1+mKvGX9Deo=";
|
||||||
|
|
||||||
|
buildCommand = ''
|
||||||
|
export HOME=$(mktemp -d)
|
||||||
|
xwin --accept-license \
|
||||||
|
--sdk-version ${windowsSdkVersion} \
|
||||||
|
--crt-version ${windowsCrtVersion} \
|
||||||
|
splat --output $out
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
mkMsvcEnv =
|
||||||
|
{ pkgs, windowsSdk }:
|
||||||
|
let
|
||||||
|
clangVersion = pkgs.lib.versions.major pkgs.llvmPackages.clang.version;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# linker for the windows target
|
||||||
|
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER = "lld-link";
|
||||||
|
|
||||||
|
# c/c++ compiler
|
||||||
|
CC_x86_64_pc_windows_msvc = "clang-cl";
|
||||||
|
CXX_x86_64_pc_windows_msvc = "clang-cl";
|
||||||
|
AR_x86_64_pc_windows_msvc = "llvm-lib";
|
||||||
|
|
||||||
|
# IMPORTANT: libclang include path MUST come first to avoid header conflicts
|
||||||
|
CFLAGS_x86_64_pc_windows_msvc = builtins.concatStringsSep " " [
|
||||||
|
"--target=x86_64-pc-windows-msvc"
|
||||||
|
"-Wno-unused-command-line-argument"
|
||||||
|
"-fuse-ld=lld-link"
|
||||||
|
"/imsvc${pkgs.llvmPackages.libclang.lib}/lib/clang/${clangVersion}/include"
|
||||||
|
"/imsvc${windowsSdk}/crt/include"
|
||||||
|
"/imsvc${windowsSdk}/sdk/include/ucrt"
|
||||||
|
"/imsvc${windowsSdk}/sdk/include/um"
|
||||||
|
"/imsvc${windowsSdk}/sdk/include/shared"
|
||||||
|
];
|
||||||
|
|
||||||
|
CXXFLAGS_x86_64_pc_windows_msvc = builtins.concatStringsSep " " [
|
||||||
|
"--target=x86_64-pc-windows-msvc"
|
||||||
|
"-Wno-unused-command-line-argument"
|
||||||
|
"-fuse-ld=lld-link"
|
||||||
|
"/imsvc${pkgs.llvmPackages.libclang.lib}/lib/clang/${clangVersion}/include"
|
||||||
|
"/imsvc${windowsSdk}/crt/include"
|
||||||
|
"/imsvc${windowsSdk}/sdk/include/ucrt"
|
||||||
|
"/imsvc${windowsSdk}/sdk/include/um"
|
||||||
|
"/imsvc${windowsSdk}/sdk/include/shared"
|
||||||
|
];
|
||||||
|
|
||||||
|
# target-specific rust flags with linker flavor and library search paths
|
||||||
|
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = builtins.concatStringsSep " " [
|
||||||
|
"-Clinker-flavor=lld-link"
|
||||||
|
"-Lnative=${windowsSdk}/crt/lib/x86_64"
|
||||||
|
"-Lnative=${windowsSdk}/sdk/lib/um/x86_64"
|
||||||
|
"-Lnative=${windowsSdk}/sdk/lib/ucrt/x86_64"
|
||||||
|
];
|
||||||
|
|
||||||
|
# cargo target
|
||||||
|
CARGO_BUILD_TARGET = "x86_64-pc-windows-msvc";
|
||||||
|
};
|
||||||
|
|
||||||
|
mkKomorebiPackages =
|
||||||
|
{ pkgs, windowsSdk }:
|
||||||
|
let
|
||||||
|
# toolchain with windows msvc target
|
||||||
|
toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override {
|
||||||
|
targets = [ "x86_64-pc-windows-msvc" ];
|
||||||
|
};
|
||||||
|
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
msvcEnv = mkMsvcEnv { inherit pkgs windowsSdk; };
|
||||||
|
|
||||||
|
src = pkgs.lib.cleanSourceWith {
|
||||||
|
src = ./.;
|
||||||
|
filter =
|
||||||
|
path: type:
|
||||||
|
(craneLib.filterCargoSources path type)
|
||||||
|
|| (pkgs.lib.hasInfix "/docs/" path)
|
||||||
|
|| (builtins.match ".*/docs/.*" path != null);
|
||||||
|
};
|
||||||
|
|
||||||
|
commonArgs = {
|
||||||
|
inherit src version;
|
||||||
|
strictDeps = true;
|
||||||
|
COMMIT_HASH = self.rev or (pkgs.lib.removeSuffix "-dirty" self.dirtyRev);
|
||||||
|
|
||||||
|
# build inputs for cross-compilation
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.llvmPackages.clang-unwrapped
|
||||||
|
pkgs.llvmPackages.lld
|
||||||
|
pkgs.llvmPackages.llvm
|
||||||
|
];
|
||||||
|
|
||||||
|
# cross-compilation environment
|
||||||
|
inherit (msvcEnv)
|
||||||
|
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER
|
||||||
|
CC_x86_64_pc_windows_msvc
|
||||||
|
CXX_x86_64_pc_windows_msvc
|
||||||
|
AR_x86_64_pc_windows_msvc
|
||||||
|
CFLAGS_x86_64_pc_windows_msvc
|
||||||
|
CXXFLAGS_x86_64_pc_windows_msvc
|
||||||
|
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS
|
||||||
|
CARGO_BUILD_TARGET
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||||
|
|
||||||
|
individualCrateArgs = commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
doCheck = false;
|
||||||
|
doDoc = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
fullBuild = craneLib.buildPackage (
|
||||||
|
individualCrateArgs
|
||||||
|
// {
|
||||||
|
pname = "komorebi-workspace";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
extractBinary =
|
||||||
|
binaryName:
|
||||||
|
pkgs.runCommand "komorebi-${binaryName}"
|
||||||
|
{
|
||||||
|
meta = fullBuild.meta // { };
|
||||||
|
}
|
||||||
|
''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cp ${fullBuild}/bin/${binaryName}.exe $out/bin/
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit
|
||||||
|
craneLib
|
||||||
|
src
|
||||||
|
individualCrateArgs
|
||||||
|
fullBuild
|
||||||
|
msvcEnv
|
||||||
|
;
|
||||||
|
komorebi = extractBinary "komorebi";
|
||||||
|
komorebic = extractBinary "komorebic";
|
||||||
|
komorebic-no-console = extractBinary "komorebic-no-console";
|
||||||
|
komorebi-bar = extractBinary "komorebi-bar";
|
||||||
|
komorebi-gui = extractBinary "komorebi-gui";
|
||||||
|
komorebi-shortcuts = extractBinary "komorebi-shortcuts";
|
||||||
|
};
|
||||||
|
|
||||||
|
mkPkgs =
|
||||||
|
system:
|
||||||
|
import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ (import rust-overlay) ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
systems = [
|
||||||
|
"aarch64-darwin"
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
];
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
inputs.treefmt-nix.flakeModule
|
||||||
|
inputs.git-hooks-nix.flakeModule
|
||||||
|
];
|
||||||
|
|
||||||
|
perSystem =
|
||||||
|
{ config, system, ... }:
|
||||||
|
let
|
||||||
|
pkgs = mkPkgs system;
|
||||||
|
windowsSdk = mkWindowsSdk pkgs;
|
||||||
|
build = mkKomorebiPackages { inherit pkgs windowsSdk; };
|
||||||
|
|
||||||
|
# toolchain with windows target and nightly rustfmt
|
||||||
|
rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override {
|
||||||
|
targets = [ "x86_64-pc-windows-msvc" ];
|
||||||
|
};
|
||||||
|
nightlyRustfmt = pkgs.rust-bin.nightly.latest.rustfmt;
|
||||||
|
rustToolchainWithNightlyRustfmt = pkgs.symlinkJoin {
|
||||||
|
name = "rust-toolchain-with-nightly-rustfmt";
|
||||||
|
paths = [
|
||||||
|
nightlyRustfmt
|
||||||
|
rustToolchain
|
||||||
|
];
|
||||||
|
};
|
||||||
|
nightlyToolchain = pkgs.rust-bin.nightly.latest.default.override {
|
||||||
|
targets = [ "x86_64-pc-windows-msvc" ];
|
||||||
|
};
|
||||||
|
cargo-udeps = pkgs.writeShellScriptBin "cargo-udeps" ''
|
||||||
|
export PATH="${nightlyToolchain}/bin:$PATH"
|
||||||
|
exec ${pkgs.cargo-udeps}/bin/cargo-udeps "$@"
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
treefmt = {
|
||||||
|
projectRootFile = "flake.nix";
|
||||||
|
programs = {
|
||||||
|
deadnix.enable = true;
|
||||||
|
just.enable = true;
|
||||||
|
nixfmt.enable = true;
|
||||||
|
taplo.enable = true;
|
||||||
|
rustfmt = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.rust-bin.nightly.latest.rustfmt;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
komorebi-workspace-clippy = build.craneLib.cargoClippy (
|
||||||
|
build.individualCrateArgs
|
||||||
|
// {
|
||||||
|
cargoClippyExtraArgs = "--all-targets -- -D warnings";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
komorebi-workspace-fmt = build.craneLib.cargoFmt {
|
||||||
|
inherit (build) src;
|
||||||
|
};
|
||||||
|
|
||||||
|
komorebi-workspace-toml-fmt = build.craneLib.taploFmt {
|
||||||
|
src = pkgs.lib.sources.sourceFilesBySuffices build.src [ ".toml" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
komorebi-workspace-deny = build.craneLib.cargoDeny {
|
||||||
|
inherit (build) src;
|
||||||
|
};
|
||||||
|
|
||||||
|
komorebi-workspace-nextest = build.craneLib.cargoNextest build.individualCrateArgs;
|
||||||
|
};
|
||||||
|
|
||||||
|
packages = {
|
||||||
|
inherit (build)
|
||||||
|
komorebi
|
||||||
|
komorebic
|
||||||
|
komorebic-no-console
|
||||||
|
komorebi-bar
|
||||||
|
komorebi-gui
|
||||||
|
komorebi-shortcuts
|
||||||
|
;
|
||||||
|
inherit windowsSdk;
|
||||||
|
komorebi-full = build.fullBuild;
|
||||||
|
default = build.fullBuild;
|
||||||
|
};
|
||||||
|
|
||||||
|
apps = {
|
||||||
|
komorebi = {
|
||||||
|
type = "app";
|
||||||
|
program = "${build.komorebi}/bin/komorebi.exe";
|
||||||
|
};
|
||||||
|
komorebic = {
|
||||||
|
type = "app";
|
||||||
|
program = "${build.komorebic}/bin/komorebic.exe";
|
||||||
|
};
|
||||||
|
komorebic-no-console = {
|
||||||
|
type = "app";
|
||||||
|
program = "${build.komorebic-no-console}/bin/komorebic-no-console.exe";
|
||||||
|
};
|
||||||
|
komorebi-bar = {
|
||||||
|
type = "app";
|
||||||
|
program = "${build.komorebi-bar}/bin/komorebi-bar.exe";
|
||||||
|
};
|
||||||
|
komorebi-gui = {
|
||||||
|
type = "app";
|
||||||
|
program = "${build.komorebi-gui}/bin/komorebi-gui.exe";
|
||||||
|
};
|
||||||
|
komorebi-shortcuts = {
|
||||||
|
type = "app";
|
||||||
|
program = "${build.komorebi-shortcuts}/bin/komorebi-shortcuts.exe";
|
||||||
|
};
|
||||||
|
default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${build.fullBuild}/bin/komorebi.exe";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
name = "komorebi";
|
||||||
|
|
||||||
|
RUST_BACKTRACE = "full";
|
||||||
|
|
||||||
|
# cross-compilation environment
|
||||||
|
inherit (build.msvcEnv)
|
||||||
|
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER
|
||||||
|
CC_x86_64_pc_windows_msvc
|
||||||
|
CXX_x86_64_pc_windows_msvc
|
||||||
|
AR_x86_64_pc_windows_msvc
|
||||||
|
CFLAGS_x86_64_pc_windows_msvc
|
||||||
|
CXXFLAGS_x86_64_pc_windows_msvc
|
||||||
|
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS
|
||||||
|
CARGO_BUILD_TARGET
|
||||||
|
;
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
rustToolchainWithNightlyRustfmt
|
||||||
|
cargo-udeps
|
||||||
|
|
||||||
|
# cross-compilation tooling
|
||||||
|
pkgs.llvmPackages.clang-unwrapped # provides clang-cl
|
||||||
|
pkgs.llvmPackages.lld # provides lld-link
|
||||||
|
pkgs.llvmPackages.llvm # provides llvm-lib
|
||||||
|
|
||||||
|
pkgs.cargo-deny
|
||||||
|
pkgs.cargo-nextest
|
||||||
|
pkgs.cargo-outdated
|
||||||
|
pkgs.jq
|
||||||
|
pkgs.just
|
||||||
|
pkgs.prettier
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
pre-commit = {
|
||||||
|
check.enable = true;
|
||||||
|
settings.hooks.treefmt = {
|
||||||
|
enable = true;
|
||||||
|
package = config.treefmt.build.wrapper;
|
||||||
|
pass_filenames = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@ fmt:
|
|||||||
prettier --write .github/FUNDING.yml
|
prettier --write .github/FUNDING.yml
|
||||||
prettier --write .github/workflows/windows.yaml
|
prettier --write .github/workflows/windows.yaml
|
||||||
|
|
||||||
|
fix:
|
||||||
|
cargo clippy --fix --allow-dirty
|
||||||
|
|
||||||
install-targets *targets:
|
install-targets *targets:
|
||||||
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
|
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
|
||||||
|
|
||||||
@@ -72,22 +75,38 @@ trace target $RUST_LOG="trace":
|
|||||||
deadlock $RUST_LOG="trace":
|
deadlock $RUST_LOG="trace":
|
||||||
cargo +stable run --bin komorebi --locked --no-default-features --features deadlock_detection
|
cargo +stable run --bin komorebi --locked --no-default-features --features deadlock_detection
|
||||||
|
|
||||||
docgen:
|
docgen starlight:
|
||||||
cargo run --package komorebic -- docgen
|
rm {{ starlight }}/src/data/cli/windows/*.md
|
||||||
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
|
cargo run --package komorebic -- docgen --output {{ starlight }}/src/data/cli/windows
|
||||||
|
schemars-docgen ./schema.json --output {{ starlight }}/src/content/docs/reference/komorebi-windows.mdx --title "komorebi.json (Windows)" --description "komorebi for Windows configuration schema reference"
|
||||||
|
schemars-docgen ./schema.bar.json --output {{ starlight }}/src/content/docs/reference/bar-windows.mdx --title "komorebi.bar.json (Windows)" --description "komorebi-bar for Windows configuration schema reference"
|
||||||
|
|
||||||
jsonschema:
|
jsonschema:
|
||||||
cargo run --package komorebic -- static-config-schema > schema.json
|
cargo run --package komorebic -- static-config-schema > schema.json
|
||||||
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
|
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
|
||||||
cargo run --package komorebi-bar -- --schema > schema.bar.json
|
cargo run --package komorebi-bar -- --schema > schema.bar.json
|
||||||
|
|
||||||
# this part is run in a nix shell because python is a nightmare
|
version := `cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "komorebi") | .version'`
|
||||||
schemagen:
|
|
||||||
rm -rf static-config-docs bar-config-docs
|
schemapub:
|
||||||
mkdir -p static-config-docs bar-config-docs
|
rm -Force komorebi-schema
|
||||||
generate-schema-doc ./schema.json --config template_name=js_offline --config minify=false ./static-config-docs/
|
mkdir -Force komorebi-schema
|
||||||
generate-schema-doc ./schema.bar.json --config template_name=js_offline --config minify=false ./bar-config-docs/
|
cp schema.json komorebi-schema/komorebi.{{ version }}.schema.json
|
||||||
mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html
|
cp schema.bar.json komorebi-schema/komorebi.bar.{{ version }}.schema.json
|
||||||
|
npx wrangler pages deploy --project-name komorebi --branch main .\komorebi-schema
|
||||||
|
|
||||||
|
depcheck:
|
||||||
|
cargo outdated --depth 2
|
||||||
|
cargo +nightly udeps --quiet
|
||||||
|
|
||||||
|
deps:
|
||||||
|
cargo update
|
||||||
|
just depgen
|
||||||
|
|
||||||
depgen:
|
depgen:
|
||||||
|
cargo deny check
|
||||||
cargo deny list --format json | jq 'del(.unlicensed)' > dependencies.json
|
cargo deny list --format json | jq 'del(.unlicensed)' > dependencies.json
|
||||||
|
|
||||||
|
procdump:
|
||||||
|
cargo build --bin komorebi
|
||||||
|
.\procdump.exe -ma -e -x . .\target\debug\komorebi.exe
|
||||||
|
|||||||
+13
-5
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-bar"
|
name = "komorebi-bar"
|
||||||
version = "0.1.38"
|
version = "0.1.41"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -17,16 +17,18 @@ crossbeam-channel = { workspace = true }
|
|||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
dunce = { workspace = true }
|
dunce = { workspace = true }
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
egui-phosphor = "0.9"
|
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" }
|
||||||
|
egui_extras = { workspace = true }
|
||||||
font-loader = "0.11"
|
font-loader = "0.11"
|
||||||
hotwatch = { workspace = true }
|
hotwatch = { workspace = true }
|
||||||
image = "0.25"
|
image = "0.25"
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
netdev = "0.36"
|
netdev = "0.41"
|
||||||
num = "0.4"
|
num = "0.4"
|
||||||
num-derive = "0.4"
|
num-derive = "0.4"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
|
regex = "1"
|
||||||
random_word = { version = "0.5", features = ["en"] }
|
random_word = { version = "0.5", features = ["en"] }
|
||||||
reqwest = { version = "0.12", features = ["blocking"] }
|
reqwest = { version = "0.12", features = ["blocking"] }
|
||||||
schemars = { workspace = true, optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
@@ -34,6 +36,8 @@ serde = { workspace = true }
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
starship-battery = "0.10"
|
starship-battery = "0.10"
|
||||||
sysinfo = { workspace = true }
|
sysinfo = { workspace = true }
|
||||||
|
systray-util = "0.2.0"
|
||||||
|
tokio = { version = "1", features = ["rt", "sync", "time"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
which = { workspace = true }
|
which = { workspace = true }
|
||||||
@@ -44,4 +48,8 @@ windows-icons-fallback = { package = "windows-icons", git = "https://github.com/
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["schemars"]
|
default = ["schemars"]
|
||||||
schemars = ["dep:schemars", "komorebi-client/default", "komorebi-themes/default"]
|
schemars = [
|
||||||
|
"dep:schemars",
|
||||||
|
"komorebi-client/default",
|
||||||
|
"komorebi-themes/default",
|
||||||
|
]
|
||||||
|
|||||||
+88
-65
@@ -1,27 +1,29 @@
|
|||||||
use crate::config::get_individual_spacing;
|
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||||
|
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||||
|
use crate::BAR_HEIGHT;
|
||||||
|
use crate::DEFAULT_PADDING;
|
||||||
|
use crate::KomorebiEvent;
|
||||||
|
use crate::MAX_LABEL_WIDTH;
|
||||||
|
use crate::MONITOR_LEFT;
|
||||||
|
use crate::MONITOR_RIGHT;
|
||||||
|
use crate::MONITOR_TOP;
|
||||||
use crate::config::KomobarConfig;
|
use crate::config::KomobarConfig;
|
||||||
use crate::config::KomobarTheme;
|
use crate::config::KomobarTheme;
|
||||||
use crate::config::MonitorConfigOrIndex;
|
use crate::config::MonitorConfigOrIndex;
|
||||||
use crate::config::Position;
|
use crate::config::Position;
|
||||||
use crate::config::PositionConfig;
|
use crate::config::PositionConfig;
|
||||||
|
use crate::config::get_individual_spacing;
|
||||||
use crate::process_hwnd;
|
use crate::process_hwnd;
|
||||||
use crate::render::Color32Ext;
|
use crate::render::Color32Ext;
|
||||||
use crate::render::Grouping;
|
use crate::render::Grouping;
|
||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::render::RenderExt;
|
use crate::render::RenderExt;
|
||||||
|
use crate::take_widget_clicked;
|
||||||
use crate::widgets::komorebi::Komorebi;
|
use crate::widgets::komorebi::Komorebi;
|
||||||
use crate::widgets::komorebi::MonitorInfo;
|
use crate::widgets::komorebi::MonitorInfo;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use crate::widgets::widget::WidgetConfig;
|
use crate::widgets::widget::WidgetConfig;
|
||||||
use crate::KomorebiEvent;
|
use color_eyre::eyre;
|
||||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
|
||||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
|
||||||
use crate::BAR_HEIGHT;
|
|
||||||
use crate::DEFAULT_PADDING;
|
|
||||||
use crate::MAX_LABEL_WIDTH;
|
|
||||||
use crate::MONITOR_LEFT;
|
|
||||||
use crate::MONITOR_RIGHT;
|
|
||||||
use crate::MONITOR_TOP;
|
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use crossbeam_channel::TryRecvError;
|
use crossbeam_channel::TryRecvError;
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
@@ -52,16 +54,18 @@ use komorebi_client::NotificationEvent;
|
|||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use komorebi_client::SocketMessage;
|
use komorebi_client::SocketMessage;
|
||||||
use komorebi_client::VirtualDesktopNotification;
|
use komorebi_client::VirtualDesktopNotification;
|
||||||
use komorebi_themes::catppuccin_egui;
|
|
||||||
use komorebi_themes::Base16Wrapper;
|
use komorebi_themes::Base16Wrapper;
|
||||||
use komorebi_themes::Catppuccin;
|
use komorebi_themes::Catppuccin;
|
||||||
|
use komorebi_themes::KomobarThemeBase16;
|
||||||
|
use komorebi_themes::KomobarThemeCatppuccin;
|
||||||
|
use komorebi_themes::KomobarThemeCustom;
|
||||||
|
use komorebi_themes::catppuccin_egui;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Error;
|
use std::io::Error;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::io::Result;
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -69,8 +73,8 @@ use std::process::ChildStdin;
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||||
|
|
||||||
@@ -78,7 +82,7 @@ lazy_static! {
|
|||||||
static ref SESSION_STDIN: Mutex<Option<ChildStdin>> = Mutex::new(None);
|
static ref SESSION_STDIN: Mutex<Option<ChildStdin>> = Mutex::new(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_powershell() -> Result<()> {
|
fn start_powershell() -> eyre::Result<()> {
|
||||||
// found running session, do nothing
|
// found running session, do nothing
|
||||||
if SESSION_STDIN.lock().as_mut().is_some() {
|
if SESSION_STDIN.lock().as_mut().is_some() {
|
||||||
tracing::debug!("PowerShell session already started");
|
tracing::debug!("PowerShell session already started");
|
||||||
@@ -102,17 +106,17 @@ fn start_powershell() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop_powershell() -> Result<()> {
|
fn stop_powershell() -> eyre::Result<()> {
|
||||||
tracing::debug!("Stopping PowerShell session");
|
tracing::debug!("Stopping PowerShell session");
|
||||||
|
|
||||||
if let Some(mut session_stdin) = SESSION_STDIN.lock().take() {
|
if let Some(mut session_stdin) = SESSION_STDIN.lock().take() {
|
||||||
if let Err(e) = session_stdin.write_all(b"exit\n") {
|
if let Err(e) = session_stdin.write_all(b"exit\n") {
|
||||||
tracing::error!(error = %e, "failed to write exit command to PowerShell stdin");
|
tracing::error!(error = %e, "failed to write exit command to PowerShell stdin");
|
||||||
return Err(e);
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
if let Err(e) = session_stdin.flush() {
|
if let Err(e) = session_stdin.flush() {
|
||||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||||
return Err(e);
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("PowerShell session stopped");
|
tracing::debug!("PowerShell session stopped");
|
||||||
@@ -123,25 +127,22 @@ fn stop_powershell() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec_powershell(cmd: &str) -> Result<()> {
|
pub fn exec_powershell(cmd: &str) -> eyre::Result<()> {
|
||||||
if let Some(session_stdin) = SESSION_STDIN.lock().as_mut() {
|
if let Some(session_stdin) = SESSION_STDIN.lock().as_mut() {
|
||||||
if let Err(e) = writeln!(session_stdin, "{cmd}") {
|
if let Err(e) = writeln!(session_stdin, "{cmd}") {
|
||||||
tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin");
|
tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin");
|
||||||
return Err(e);
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = session_stdin.flush() {
|
if let Err(e) = session_stdin.flush() {
|
||||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||||
return Err(e);
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(Error::new(
|
Err(Error::new(ErrorKind::NotFound, "PowerShell session not started").into())
|
||||||
ErrorKind::NotFound,
|
|
||||||
"PowerShell session not started",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Komobar {
|
pub struct Komobar {
|
||||||
@@ -186,12 +187,12 @@ pub fn apply_theme(
|
|||||||
render_config: Rc<RefCell<RenderConfig>>,
|
render_config: Rc<RefCell<RenderConfig>>,
|
||||||
) {
|
) {
|
||||||
let (auto_select_fill, auto_select_text) = match theme {
|
let (auto_select_fill, auto_select_text) = match theme {
|
||||||
KomobarTheme::Catppuccin {
|
KomobarTheme::Catppuccin(KomobarThemeCatppuccin {
|
||||||
name: catppuccin,
|
name: catppuccin,
|
||||||
accent: catppuccin_value,
|
accent: catppuccin_value,
|
||||||
auto_select_fill: catppuccin_auto_select_fill,
|
auto_select_fill: catppuccin_auto_select_fill,
|
||||||
auto_select_text: catppuccin_auto_select_text,
|
auto_select_text: catppuccin_auto_select_text,
|
||||||
} => {
|
}) => {
|
||||||
match catppuccin {
|
match catppuccin {
|
||||||
Catppuccin::Frappe => {
|
Catppuccin::Frappe => {
|
||||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
|
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
|
||||||
@@ -256,12 +257,12 @@ pub fn apply_theme(
|
|||||||
catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())),
|
catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
KomobarTheme::Base16 {
|
KomobarTheme::Base16(KomobarThemeBase16 {
|
||||||
name: base16,
|
name: base16,
|
||||||
accent: base16_value,
|
accent: base16_value,
|
||||||
auto_select_fill: base16_auto_select_fill,
|
auto_select_fill: base16_auto_select_fill,
|
||||||
auto_select_text: base16_auto_select_text,
|
auto_select_text: base16_auto_select_text,
|
||||||
} => {
|
}) => {
|
||||||
ctx.set_style(base16.style());
|
ctx.set_style(base16.style());
|
||||||
let base16_value = base16_value.unwrap_or_default();
|
let base16_value = base16_value.unwrap_or_default();
|
||||||
let accent = base16_value.color32(Base16Wrapper::Base16(base16));
|
let accent = base16_value.color32(Base16Wrapper::Base16(base16));
|
||||||
@@ -279,12 +280,12 @@ pub fn apply_theme(
|
|||||||
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))),
|
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
KomobarTheme::Custom {
|
KomobarTheme::Custom(KomobarThemeCustom {
|
||||||
colours,
|
colours,
|
||||||
accent: base16_value,
|
accent: base16_value,
|
||||||
auto_select_fill: base16_auto_select_fill,
|
auto_select_fill: base16_auto_select_fill,
|
||||||
auto_select_text: base16_auto_select_text,
|
auto_select_text: base16_auto_select_text,
|
||||||
} => {
|
}) => {
|
||||||
let background = colours.background();
|
let background = colours.background();
|
||||||
ctx.set_style(colours.style());
|
ctx.set_style(colours.style());
|
||||||
let base16_value = base16_value.unwrap_or_default();
|
let base16_value = base16_value.unwrap_or_default();
|
||||||
@@ -322,16 +323,15 @@ pub fn apply_theme(
|
|||||||
// apply rounding to the widgets
|
// apply rounding to the widgets
|
||||||
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) =
|
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) =
|
||||||
&grouping
|
&grouping
|
||||||
|
&& let Some(rounding) = config.rounding
|
||||||
{
|
{
|
||||||
if let Some(rounding) = config.rounding {
|
ctx.style_mut(|style| {
|
||||||
ctx.style_mut(|style| {
|
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
|
||||||
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
|
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update RenderConfig's background_color so that widgets will have the new color
|
// Update RenderConfig's background_color so that widgets will have the new color
|
||||||
@@ -454,10 +454,11 @@ impl Komobar {
|
|||||||
self.right_widgets = right_widgets;
|
self.right_widgets = right_widgets;
|
||||||
|
|
||||||
let (usr_monitor_index, config_work_area_offset) = match &self.config.monitor {
|
let (usr_monitor_index, config_work_area_offset) = match &self.config.monitor {
|
||||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
|
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
|
||||||
(monitor_config.index, monitor_config.work_area_offset)
|
(monitor_config.index, monitor_config.work_area_offset)
|
||||||
}
|
}
|
||||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None),
|
||||||
|
None => (0, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mapped_info = self.monitor_info.as_ref().map(|info| {
|
let mapped_info = self.monitor_info.as_ref().map(|info| {
|
||||||
@@ -523,10 +524,14 @@ impl Komobar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if self.monitor_info.is_some() && !self.disabled {
|
} else if self.monitor_info.is_some() && !self.disabled {
|
||||||
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
|
tracing::warn!(
|
||||||
|
"couldn't find the monitor index of this bar! Disabling the bar until the monitor connects..."
|
||||||
|
);
|
||||||
self.disabled = true;
|
self.disabled = true;
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi.");
|
tracing::warn!(
|
||||||
|
"couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi."
|
||||||
|
);
|
||||||
self.disabled = true;
|
self.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,7 +604,9 @@ impl Komobar {
|
|||||||
end.x -= margin.left + margin.right;
|
end.x -= margin.left + margin.right;
|
||||||
|
|
||||||
if end.y == 0.0 {
|
if end.y == 0.0 {
|
||||||
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
|
tracing::warn!(
|
||||||
|
"position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.size_rect = komorebi_client::Rect {
|
self.size_rect = komorebi_client::Rect {
|
||||||
@@ -666,17 +673,16 @@ impl Komobar {
|
|||||||
| Grouping::Alignment(config)
|
| Grouping::Alignment(config)
|
||||||
| Grouping::Widget(config),
|
| Grouping::Widget(config),
|
||||||
) = &bar_grouping
|
) = &bar_grouping
|
||||||
|
&& let Some(rounding) = config.rounding
|
||||||
{
|
{
|
||||||
if let Some(rounding) = config.rounding {
|
ctx.style_mut(|style| {
|
||||||
ctx.style_mut(|style| {
|
style.visuals.widgets.noninteractive.corner_radius =
|
||||||
style.visuals.widgets.noninteractive.corner_radius =
|
rounding.into();
|
||||||
rounding.into();
|
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -862,9 +868,13 @@ impl eframe::App for Komobar {
|
|||||||
Ok(KomorebiEvent::Notification(notification)) => {
|
Ok(KomorebiEvent::Notification(notification)) => {
|
||||||
let state = ¬ification.state;
|
let state = ¬ification.state;
|
||||||
let usr_monitor_index = match &self.config.monitor {
|
let usr_monitor_index = match &self.config.monitor {
|
||||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
|
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
|
||||||
MonitorConfigOrIndex::Index(idx) => *idx,
|
monitor_config.index
|
||||||
|
}
|
||||||
|
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
|
||||||
|
None => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied();
|
let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied();
|
||||||
self.monitor_index = monitor_index;
|
self.monitor_index = monitor_index;
|
||||||
let mut should_apply_config = false;
|
let mut should_apply_config = false;
|
||||||
@@ -937,9 +947,9 @@ impl eframe::App for Komobar {
|
|||||||
) {
|
) {
|
||||||
let monitor_index = self.monitor_index.expect("should have a monitor index");
|
let monitor_index = self.monitor_index.expect("should have a monitor index");
|
||||||
|
|
||||||
let monitor_size = state.monitors.elements()[monitor_index].size();
|
let monitor_size = state.monitors.elements()[monitor_index].size;
|
||||||
|
|
||||||
self.update_monitor_coordinates(monitor_size);
|
self.update_monitor_coordinates(&monitor_size);
|
||||||
|
|
||||||
should_apply_config = true;
|
should_apply_config = true;
|
||||||
}
|
}
|
||||||
@@ -950,7 +960,7 @@ impl eframe::App for Komobar {
|
|||||||
|
|
||||||
// Check if monitor coordinates/size has changed
|
// Check if monitor coordinates/size has changed
|
||||||
if let Some(monitor_index) = self.monitor_index {
|
if let Some(monitor_index) = self.monitor_index {
|
||||||
let monitor_size = state.monitors.elements()[monitor_index].size();
|
let monitor_size = state.monitors.elements()[monitor_index].size;
|
||||||
let top = MONITOR_TOP.load(Ordering::SeqCst);
|
let top = MONITOR_TOP.load(Ordering::SeqCst);
|
||||||
let left = MONITOR_LEFT.load(Ordering::SeqCst);
|
let left = MONITOR_LEFT.load(Ordering::SeqCst);
|
||||||
let right = MONITOR_RIGHT.load(Ordering::SeqCst);
|
let right = MONITOR_RIGHT.load(Ordering::SeqCst);
|
||||||
@@ -960,13 +970,13 @@ impl eframe::App for Komobar {
|
|||||||
bottom: monitor_size.bottom,
|
bottom: monitor_size.bottom,
|
||||||
right,
|
right,
|
||||||
};
|
};
|
||||||
if *monitor_size != rect {
|
if monitor_size != rect {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Monitor coordinates/size has changed, storing new coordinates: {:#?}",
|
"Monitor coordinates/size has changed, storing new coordinates: {:#?}",
|
||||||
monitor_size
|
monitor_size
|
||||||
);
|
);
|
||||||
|
|
||||||
self.update_monitor_coordinates(monitor_size);
|
self.update_monitor_coordinates(&monitor_size);
|
||||||
|
|
||||||
should_apply_config = true;
|
should_apply_config = true;
|
||||||
}
|
}
|
||||||
@@ -1073,6 +1083,10 @@ impl eframe::App for Komobar {
|
|||||||
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
||||||
|
|
||||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
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 {
|
if let Some(mouse_config) = &self.config.mouse {
|
||||||
let command = if ui
|
let command = if ui
|
||||||
.input(|i| i.pointer.button_double_clicked(PointerButton::Primary))
|
.input(|i| i.pointer.button_double_clicked(PointerButton::Primary))
|
||||||
@@ -1173,9 +1187,9 @@ impl eframe::App for Komobar {
|
|||||||
&None
|
&None
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(command) = command {
|
// Store the command to execute after widgets are rendered
|
||||||
command.execute(self.mouse_follows_focus);
|
// This allows widgets to mark clicks as consumed
|
||||||
}
|
pending_command = command.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply grouping logic for the bar as a whole
|
// Apply grouping logic for the bar as a whole
|
||||||
@@ -1307,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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1358,7 +1379,9 @@ fn handle_notification(
|
|||||||
"removed theme from updated komorebi.json and applied default theme"
|
"removed theme from updated komorebi.json and applied default theme"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
|
tracing::warn!(
|
||||||
|
"theme was removed from updated komorebi.json but there was no default theme to apply"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-80
@@ -1,11 +1,10 @@
|
|||||||
|
use crate::DEFAULT_PADDING;
|
||||||
use crate::bar::exec_powershell;
|
use crate::bar::exec_powershell;
|
||||||
use crate::render::Grouping;
|
use crate::render::Grouping;
|
||||||
use crate::widgets::widget::WidgetConfig;
|
use crate::widgets::widget::WidgetConfig;
|
||||||
use crate::DEFAULT_PADDING;
|
|
||||||
use eframe::egui::Pos2;
|
use eframe::egui::Pos2;
|
||||||
use eframe::egui::TextBuffer;
|
use eframe::egui::TextBuffer;
|
||||||
use eframe::egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
use komorebi_client::KomorebiTheme;
|
|
||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use komorebi_client::Rect;
|
use komorebi_client::Rect;
|
||||||
use komorebi_client::SocketMessage;
|
use komorebi_client::SocketMessage;
|
||||||
@@ -16,9 +15,10 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.38`
|
/// The `komorebi.bar.json` configuration file reference for `v0.1.41`
|
||||||
pub struct KomobarConfig {
|
pub struct KomobarConfig {
|
||||||
/// Bar height (default: 50)
|
/// Bar height
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50)))]
|
||||||
pub height: Option<f32>,
|
pub height: Option<f32>,
|
||||||
/// Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or
|
/// Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or
|
||||||
/// vertical definition which can each take a single value for a symmetric padding or two
|
/// vertical definition which can each take a single value for a symmetric padding or two
|
||||||
@@ -76,20 +76,26 @@ pub struct KomobarConfig {
|
|||||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
|
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
|
||||||
pub frame: Option<FrameConfig>,
|
pub frame: Option<FrameConfig>,
|
||||||
/// The monitor index or the full monitor options
|
/// The monitor index or the full monitor options
|
||||||
pub monitor: MonitorConfigOrIndex,
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = MonitorConfigOrIndex::Index(0))))]
|
||||||
|
pub monitor: Option<MonitorConfigOrIndex>,
|
||||||
/// Font family
|
/// Font family
|
||||||
pub font_family: Option<String>,
|
pub font_family: Option<String>,
|
||||||
/// Font size (default: 12.5)
|
/// Font size
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12.5)))]
|
||||||
pub font_size: Option<f32>,
|
pub font_size: Option<f32>,
|
||||||
/// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
|
/// Scale of the icons relative to the font_size [[1.0-2.0]]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 1.4)))]
|
||||||
pub icon_scale: Option<f32>,
|
pub icon_scale: Option<f32>,
|
||||||
/// Max label width before text truncation (default: 400.0)
|
/// Max label width before text truncation
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 400.0)))]
|
||||||
pub max_label_width: Option<f32>,
|
pub max_label_width: Option<f32>,
|
||||||
/// Theme
|
/// Theme
|
||||||
pub theme: Option<KomobarTheme>,
|
pub theme: Option<KomobarTheme>,
|
||||||
/// Alpha value for the color transparency [[0-255]] (default: 200)
|
/// Alpha value for the color transparency [[0-255]]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 200)))]
|
||||||
pub transparency_alpha: Option<u8>,
|
pub transparency_alpha: Option<u8>,
|
||||||
/// Spacing between widgets (default: 10.0)
|
/// Spacing between widgets
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10.0)))]
|
||||||
pub widget_spacing: Option<f32>,
|
pub widget_spacing: Option<f32>,
|
||||||
/// Visual grouping for widgets
|
/// Visual grouping for widgets
|
||||||
pub grouping: Option<Grouping>,
|
pub grouping: Option<Grouping>,
|
||||||
@@ -120,7 +126,9 @@ impl KomobarConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if display {
|
if display {
|
||||||
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
|
println!(
|
||||||
|
"\nYour bar configuration file contains some options that have been renamed or deprecated:\n"
|
||||||
|
);
|
||||||
for (canonical, aliases) in map {
|
for (canonical, aliases) in map {
|
||||||
for alias in aliases {
|
for alias in aliases {
|
||||||
if raw.contains(alias) {
|
if raw.contains(alias) {
|
||||||
@@ -143,6 +151,7 @@ impl KomobarConfig {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Position configuration
|
||||||
pub struct PositionConfig {
|
pub struct PositionConfig {
|
||||||
/// The desired starting position of the bar (0,0 = top left of the screen)
|
/// The desired starting position of the bar (0,0 = top left of the screen)
|
||||||
#[serde(alias = "position")]
|
#[serde(alias = "position")]
|
||||||
@@ -154,6 +163,7 @@ pub struct PositionConfig {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Frame configuration
|
||||||
pub struct FrameConfig {
|
pub struct FrameConfig {
|
||||||
/// Margin inside the painted frame
|
/// Margin inside the painted frame
|
||||||
pub inner_margin: Position,
|
pub inner_margin: Position,
|
||||||
@@ -162,6 +172,7 @@ pub struct FrameConfig {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
/// Monitor configuration or monitor index
|
||||||
pub enum MonitorConfigOrIndex {
|
pub enum MonitorConfigOrIndex {
|
||||||
/// The monitor index where you want the bar to show
|
/// The monitor index where you want the bar to show
|
||||||
Index(usize),
|
Index(usize),
|
||||||
@@ -171,6 +182,7 @@ pub enum MonitorConfigOrIndex {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Monitor configuration
|
||||||
pub struct MonitorConfig {
|
pub struct MonitorConfig {
|
||||||
/// Komorebi monitor index of the monitor on which to render the bar
|
/// Komorebi monitor index of the monitor on which to render the bar
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
@@ -188,9 +200,13 @@ pub type Margin = SpacingKind;
|
|||||||
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
|
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
|
||||||
// `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the
|
// `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the
|
||||||
// individual values.
|
// individual values.
|
||||||
|
/// Spacing kind
|
||||||
pub enum SpacingKind {
|
pub enum SpacingKind {
|
||||||
|
/// Spacing applied to all sides
|
||||||
All(f32),
|
All(f32),
|
||||||
|
/// Individual spacing applied to each side
|
||||||
Individual(IndividualSpacingConfig),
|
Individual(IndividualSpacingConfig),
|
||||||
|
/// Grouped vertical and horizontal spacing
|
||||||
Grouped(GroupedSpacingConfig),
|
Grouped(GroupedSpacingConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,25 +251,36 @@ impl SpacingKind {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Grouped vertical and horizontal spacing
|
||||||
pub struct GroupedSpacingConfig {
|
pub struct GroupedSpacingConfig {
|
||||||
|
/// Vertical grouped spacing
|
||||||
pub vertical: Option<GroupedSpacingOptions>,
|
pub vertical: Option<GroupedSpacingOptions>,
|
||||||
|
/// Horizontal grouped spacing
|
||||||
pub horizontal: Option<GroupedSpacingOptions>,
|
pub horizontal: Option<GroupedSpacingOptions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
/// Grouped spacing options
|
||||||
pub enum GroupedSpacingOptions {
|
pub enum GroupedSpacingOptions {
|
||||||
|
/// Symmetrical grouped spacing
|
||||||
Symmetrical(f32),
|
Symmetrical(f32),
|
||||||
|
/// Split grouped spacing
|
||||||
Split(f32, f32),
|
Split(f32, f32),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Individual spacing configuration
|
||||||
pub struct IndividualSpacingConfig {
|
pub struct IndividualSpacingConfig {
|
||||||
|
/// Spacing for the top
|
||||||
pub top: f32,
|
pub top: f32,
|
||||||
|
/// Spacing for the bottom
|
||||||
pub bottom: f32,
|
pub bottom: f32,
|
||||||
|
/// Spacing for the left
|
||||||
pub left: f32,
|
pub left: f32,
|
||||||
|
/// Spacing for the right
|
||||||
pub right: f32,
|
pub right: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +360,7 @@ pub fn get_individual_spacing(
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
/// Mouse message
|
||||||
pub enum MouseMessage {
|
pub enum MouseMessage {
|
||||||
/// Send a message to the komorebi client.
|
/// Send a message to the komorebi client.
|
||||||
/// By default, a batch of messages are sent in the following order:
|
/// By default, a batch of messages are sent in the following order:
|
||||||
@@ -377,10 +405,13 @@ pub enum MouseMessage {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Komorebi socket mouse message
|
||||||
pub struct KomorebiMouseMessage {
|
pub struct KomorebiMouseMessage {
|
||||||
/// Send the FocusMonitorAtCursor message (default:true)
|
/// Send the FocusMonitorAtCursor message
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||||
pub focus_monitor_at_cursor: Option<bool>,
|
pub focus_monitor_at_cursor: Option<bool>,
|
||||||
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message (default:true)
|
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||||
pub ignore_mouse_follows_focus: Option<bool>,
|
pub ignore_mouse_follows_focus: Option<bool>,
|
||||||
/// The message to send to the komorebi client
|
/// The message to send to the komorebi client
|
||||||
pub message: komorebi_client::SocketMessage,
|
pub message: komorebi_client::SocketMessage,
|
||||||
@@ -388,6 +419,7 @@ pub struct KomorebiMouseMessage {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Mouse configuration
|
||||||
pub struct MouseConfig {
|
pub struct MouseConfig {
|
||||||
/// Command to send on primary/left double button click
|
/// Command to send on primary/left double button click
|
||||||
pub on_primary_double_click: Option<MouseMessage>,
|
pub on_primary_double_click: Option<MouseMessage>,
|
||||||
@@ -400,14 +432,16 @@ pub struct MouseConfig {
|
|||||||
/// Command to send on extra2/forward button click
|
/// Command to send on extra2/forward button click
|
||||||
pub on_extra2_click: Option<MouseMessage>,
|
pub on_extra2_click: Option<MouseMessage>,
|
||||||
|
|
||||||
/// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
/// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 30.0)))]
|
||||||
pub vertical_scroll_threshold: Option<f32>,
|
pub vertical_scroll_threshold: Option<f32>,
|
||||||
/// Command to send on scrolling up (every tick)
|
/// Command to send on scrolling up (every tick)
|
||||||
pub on_scroll_up: Option<MouseMessage>,
|
pub on_scroll_up: Option<MouseMessage>,
|
||||||
/// Command to send on scrolling down (every tick)
|
/// Command to send on scrolling down (every tick)
|
||||||
pub on_scroll_down: Option<MouseMessage>,
|
pub on_scroll_down: Option<MouseMessage>,
|
||||||
|
|
||||||
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 30.0)))]
|
||||||
pub horizontal_scroll_threshold: Option<f32>,
|
pub horizontal_scroll_threshold: Option<f32>,
|
||||||
/// Command to send on scrolling left (every tick)
|
/// Command to send on scrolling left (every tick)
|
||||||
pub on_scroll_left: Option<MouseMessage>,
|
pub on_scroll_left: Option<MouseMessage>,
|
||||||
@@ -453,7 +487,7 @@ impl MouseMessage {
|
|||||||
|
|
||||||
tracing::debug!("Sending messages: {messages:?}");
|
tracing::debug!("Sending messages: {messages:?}");
|
||||||
|
|
||||||
if komorebi_client::send_batch(messages.into_iter()).is_err() {
|
if komorebi_client::send_batch(messages).is_err() {
|
||||||
tracing::error!("could not send commands");
|
tracing::error!("could not send commands");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,6 +527,7 @@ impl KomobarConfig {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Position
|
||||||
pub struct Position {
|
pub struct Position {
|
||||||
/// X coordinate
|
/// X coordinate
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
@@ -518,71 +553,11 @@ impl From<Position> for Pos2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
pub use komorebi_themes::KomobarTheme;
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
#[serde(tag = "palette")]
|
|
||||||
pub enum KomobarTheme {
|
|
||||||
/// A theme from catppuccin-egui
|
|
||||||
Catppuccin {
|
|
||||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
|
||||||
name: komorebi_themes::Catppuccin,
|
|
||||||
accent: Option<komorebi_themes::CatppuccinValue>,
|
|
||||||
auto_select_fill: Option<komorebi_themes::CatppuccinValue>,
|
|
||||||
auto_select_text: Option<komorebi_themes::CatppuccinValue>,
|
|
||||||
},
|
|
||||||
/// A theme from base16-egui-themes
|
|
||||||
Base16 {
|
|
||||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
|
||||||
name: komorebi_themes::Base16,
|
|
||||||
accent: Option<komorebi_themes::Base16Value>,
|
|
||||||
auto_select_fill: Option<komorebi_themes::Base16Value>,
|
|
||||||
auto_select_text: Option<komorebi_themes::Base16Value>,
|
|
||||||
},
|
|
||||||
/// A custom Base16 theme
|
|
||||||
Custom {
|
|
||||||
/// Colours of the custom Base16 theme palette
|
|
||||||
colours: Box<komorebi_themes::Base16ColourPalette>,
|
|
||||||
accent: Option<komorebi_themes::Base16Value>,
|
|
||||||
auto_select_fill: Option<komorebi_themes::Base16Value>,
|
|
||||||
auto_select_text: Option<komorebi_themes::Base16Value>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<KomorebiTheme> for KomobarTheme {
|
|
||||||
fn from(value: KomorebiTheme) -> Self {
|
|
||||||
match value {
|
|
||||||
KomorebiTheme::Catppuccin {
|
|
||||||
name, bar_accent, ..
|
|
||||||
} => Self::Catppuccin {
|
|
||||||
name,
|
|
||||||
accent: bar_accent,
|
|
||||||
auto_select_fill: None,
|
|
||||||
auto_select_text: None,
|
|
||||||
},
|
|
||||||
KomorebiTheme::Base16 {
|
|
||||||
name, bar_accent, ..
|
|
||||||
} => Self::Base16 {
|
|
||||||
name,
|
|
||||||
accent: bar_accent,
|
|
||||||
auto_select_fill: None,
|
|
||||||
auto_select_text: None,
|
|
||||||
},
|
|
||||||
KomorebiTheme::Custom {
|
|
||||||
colours,
|
|
||||||
bar_accent,
|
|
||||||
..
|
|
||||||
} => Self::Custom {
|
|
||||||
colours,
|
|
||||||
accent: bar_accent,
|
|
||||||
auto_select_fill: None,
|
|
||||||
auto_select_text: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Label prefix
|
||||||
pub enum LabelPrefix {
|
pub enum LabelPrefix {
|
||||||
/// Show no prefix
|
/// Show no prefix
|
||||||
None,
|
None,
|
||||||
@@ -596,6 +571,7 @@ pub enum LabelPrefix {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Display format
|
||||||
pub enum DisplayFormat {
|
pub enum DisplayFormat {
|
||||||
/// Show only icon
|
/// Show only icon
|
||||||
Icon,
|
Icon,
|
||||||
@@ -610,9 +586,10 @@ pub enum DisplayFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! extend_enum {
|
macro_rules! extend_enum {
|
||||||
($existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
|
($(#[$type_meta:meta])* $existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
|
||||||
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
$(#[$type_meta])*
|
||||||
pub enum $new_enum {
|
pub enum $new_enum {
|
||||||
// Add new variants
|
// Add new variants
|
||||||
$(
|
$(
|
||||||
@@ -633,7 +610,9 @@ macro_rules! extend_enum {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
|
extend_enum!(
|
||||||
|
/// Workspaces display format
|
||||||
|
DisplayFormat, WorkspacesDisplayFormat, {
|
||||||
/// Show all icons only
|
/// Show all icons only
|
||||||
AllIcons,
|
AllIcons,
|
||||||
/// Show both all icons and text
|
/// Show both all icons and text
|
||||||
@@ -642,6 +621,26 @@ extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
|
|||||||
AllIconsAndTextOnSelected,
|
AllIconsAndTextOnSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Media widget display format
|
||||||
|
pub enum MediaDisplayFormat {
|
||||||
|
/// Show only the media info icon
|
||||||
|
Icon,
|
||||||
|
/// Show only the media info text (artist - title)
|
||||||
|
Text,
|
||||||
|
/// Show both icon and text
|
||||||
|
IconAndText,
|
||||||
|
/// Show only the control buttons (previous, play/pause, next)
|
||||||
|
ControlsOnly,
|
||||||
|
/// Show icon with control buttons
|
||||||
|
IconAndControls,
|
||||||
|
/// Show text with control buttons
|
||||||
|
TextAndControls,
|
||||||
|
/// Show icon, text, and control buttons
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|||||||
+43
-28
@@ -15,10 +15,10 @@ use eframe::egui::ViewportBuilder;
|
|||||||
use font_loader::system_fonts;
|
use font_loader::system_fonts;
|
||||||
use hotwatch::EventKind;
|
use hotwatch::EventKind;
|
||||||
use hotwatch::Hotwatch;
|
use hotwatch::Hotwatch;
|
||||||
use komorebi_client::replace_env_in_path;
|
|
||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use komorebi_client::SocketMessage;
|
use komorebi_client::SocketMessage;
|
||||||
use komorebi_client::SubscribeOptions;
|
use komorebi_client::SubscribeOptions;
|
||||||
|
use komorebi_client::replace_env_in_path;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -32,12 +32,14 @@ use windows::Win32::Foundation::HWND;
|
|||||||
use windows::Win32::Foundation::LPARAM;
|
use windows::Win32::Foundation::LPARAM;
|
||||||
use windows::Win32::System::Threading::GetCurrentProcessId;
|
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||||
use windows::Win32::System::Threading::GetCurrentThreadId;
|
use windows::Win32::System::Threading::GetCurrentThreadId;
|
||||||
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
|
|
||||||
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||||
|
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
|
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||||
use windows_core::BOOL;
|
use windows_core::BOOL;
|
||||||
|
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
|
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
|
||||||
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
|
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
|
||||||
pub static MONITOR_TOP: 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 BAR_HEIGHT: f32 = 50.0;
|
||||||
pub static DEFAULT_PADDING: f32 = 10.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_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||||
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
@@ -103,7 +119,7 @@ fn process_hwnd() -> Option<isize> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub enum KomorebiEvent {
|
pub enum KomorebiEvent {
|
||||||
Notification(komorebi_client::Notification),
|
Notification(Box<komorebi_client::Notification>),
|
||||||
Reconnect,
|
Reconnect,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,15 +130,8 @@ fn main() -> color_eyre::Result<()> {
|
|||||||
|
|
||||||
#[cfg(feature = "schemars")]
|
#[cfg(feature = "schemars")]
|
||||||
if opts.schema {
|
if opts.schema {
|
||||||
let settings = schemars::gen::SchemaSettings::default().with(|s| {
|
let bar_config = schemars::schema_for!(KomobarConfig);
|
||||||
s.option_nullable = false;
|
let schema = serde_json::to_string_pretty(&bar_config)?;
|
||||||
s.option_add_null_type = false;
|
|
||||||
s.inline_subschemas = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
let gen = settings.into_generator();
|
|
||||||
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
|
|
||||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
|
||||||
|
|
||||||
println!("{schema}");
|
println!("{schema}");
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
@@ -137,13 +146,17 @@ fn main() -> color_eyre::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
unsafe {
|
||||||
|
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
std::env::set_var("RUST_LOG", "info");
|
unsafe {
|
||||||
|
std::env::set_var("RUST_LOG", "info");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::subscriber::set_global_default(
|
tracing::subscriber::set_global_default(
|
||||||
@@ -219,28 +232,30 @@ fn main() -> color_eyre::Result<()> {
|
|||||||
)?)?;
|
)?)?;
|
||||||
|
|
||||||
let (usr_monitor_index, work_area_offset) = match &config.monitor {
|
let (usr_monitor_index, work_area_offset) = match &config.monitor {
|
||||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
|
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
|
||||||
(monitor_config.index, monitor_config.work_area_offset)
|
(monitor_config.index, monitor_config.work_area_offset)
|
||||||
}
|
}
|
||||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None),
|
||||||
|
None => (0, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let monitor_index = state
|
let monitor_index = state
|
||||||
.monitor_usr_idx_map
|
.monitor_usr_idx_map
|
||||||
.get(&usr_monitor_index)
|
.get(&usr_monitor_index)
|
||||||
.map_or(usr_monitor_index, |i| *i);
|
.map_or(usr_monitor_index, |i| *i);
|
||||||
|
|
||||||
MONITOR_RIGHT.store(
|
MONITOR_RIGHT.store(
|
||||||
state.monitors.elements()[monitor_index].size().right,
|
state.monitors.elements()[monitor_index].size.right,
|
||||||
Ordering::SeqCst,
|
Ordering::SeqCst,
|
||||||
);
|
);
|
||||||
|
|
||||||
MONITOR_TOP.store(
|
MONITOR_TOP.store(
|
||||||
state.monitors.elements()[monitor_index].size().top,
|
state.monitors.elements()[monitor_index].size.top,
|
||||||
Ordering::SeqCst,
|
Ordering::SeqCst,
|
||||||
);
|
);
|
||||||
|
|
||||||
MONITOR_LEFT.store(
|
MONITOR_LEFT.store(
|
||||||
state.monitors.elements()[monitor_index].size().left,
|
state.monitors.elements()[monitor_index].size.left,
|
||||||
Ordering::SeqCst,
|
Ordering::SeqCst,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -250,11 +265,11 @@ fn main() -> color_eyre::Result<()> {
|
|||||||
None => {
|
None => {
|
||||||
config.position = Some(PositionConfig {
|
config.position = Some(PositionConfig {
|
||||||
start: Some(Position {
|
start: Some(Position {
|
||||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
x: state.monitors.elements()[monitor_index].size.left as f32,
|
||||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
y: state.monitors.elements()[monitor_index].size.top as f32,
|
||||||
}),
|
}),
|
||||||
end: Some(Position {
|
end: Some(Position {
|
||||||
x: state.monitors.elements()[monitor_index].size().right as f32,
|
x: state.monitors.elements()[monitor_index].size.right as f32,
|
||||||
y: 50.0,
|
y: 50.0,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -262,14 +277,14 @@ fn main() -> color_eyre::Result<()> {
|
|||||||
Some(ref mut position) => {
|
Some(ref mut position) => {
|
||||||
if position.start.is_none() {
|
if position.start.is_none() {
|
||||||
position.start = Some(Position {
|
position.start = Some(Position {
|
||||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
x: state.monitors.elements()[monitor_index].size.left as f32,
|
||||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
y: state.monitors.elements()[monitor_index].size.top as f32,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if position.end.is_none() {
|
if position.end.is_none() {
|
||||||
position.end = Some(Position {
|
position.end = Some(Position {
|
||||||
x: state.monitors.elements()[monitor_index].size().right as f32,
|
x: state.monitors.elements()[monitor_index].size.right as f32,
|
||||||
y: 50.0,
|
y: 50.0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -354,7 +369,7 @@ fn main() -> color_eyre::Result<()> {
|
|||||||
while komorebi_client::send_message(
|
while komorebi_client::send_message(
|
||||||
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
|
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
|
||||||
)
|
)
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
std::thread::sleep(Duration::from_secs(1));
|
std::thread::sleep(Duration::from_secs(1));
|
||||||
}
|
}
|
||||||
@@ -377,7 +392,7 @@ fn main() -> color_eyre::Result<()> {
|
|||||||
Ok(notification) => {
|
Ok(notification) => {
|
||||||
tracing::debug!("received notification from komorebi");
|
tracing::debug!("received notification from komorebi");
|
||||||
|
|
||||||
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
|
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(Box::new(notification))) {
|
||||||
tracing::error!("could not send komorebi notification update to gui thread: {error}")
|
tracing::error!("could not send komorebi notification update to gui thread: {error}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,5 +420,5 @@ fn main() -> color_eyre::Result<()> {
|
|||||||
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
|
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||||
|
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||||
use crate::bar::Alignment;
|
use crate::bar::Alignment;
|
||||||
use crate::config::KomobarConfig;
|
use crate::config::KomobarConfig;
|
||||||
use crate::config::MonitorConfigOrIndex;
|
use crate::config::MonitorConfigOrIndex;
|
||||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
|
||||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
|
||||||
use eframe::egui::Color32;
|
use eframe::egui::Color32;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::CornerRadius;
|
use eframe::egui::CornerRadius;
|
||||||
@@ -18,27 +18,33 @@ use komorebi_client::Rgb;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::AtomicUsize;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
|
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
|
/// Grouping
|
||||||
pub enum Grouping {
|
pub enum Grouping {
|
||||||
/// No grouping is applied
|
/// No grouping is applied
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "None"))]
|
||||||
None,
|
None,
|
||||||
/// Widgets are grouped as a whole
|
/// Widgets are grouped as a whole
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Bar"))]
|
||||||
Bar(GroupingConfig),
|
Bar(GroupingConfig),
|
||||||
/// Widgets are grouped by alignment
|
/// Widgets are grouped by alignment
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Alignment"))]
|
||||||
Alignment(GroupingConfig),
|
Alignment(GroupingConfig),
|
||||||
/// Widgets are grouped individually
|
/// Widgets are grouped individually
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Widget"))]
|
||||||
Widget(GroupingConfig),
|
Widget(GroupingConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
/// Render configuration
|
||||||
pub struct RenderConfig {
|
pub struct RenderConfig {
|
||||||
/// Komorebi monitor index of the monitor on which to render the bar
|
/// Komorebi monitor index of the monitor on which to render the bar
|
||||||
pub monitor_idx: usize,
|
pub monitor_idx: usize,
|
||||||
@@ -93,8 +99,9 @@ impl RenderExt for &KomobarConfig {
|
|||||||
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
|
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
|
||||||
|
|
||||||
let monitor_idx = match &self.monitor {
|
let monitor_idx = match &self.monitor {
|
||||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
|
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => monitor_config.index,
|
||||||
MonitorConfigOrIndex::Index(idx) => *idx,
|
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
|
||||||
|
None => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
|
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
|
||||||
@@ -356,6 +363,7 @@ impl RenderConfig {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Grouping configuration
|
||||||
pub struct GroupingConfig {
|
pub struct GroupingConfig {
|
||||||
/// Styles for the grouping
|
/// Styles for the grouping
|
||||||
pub style: Option<GroupingStyle>,
|
pub style: Option<GroupingStyle>,
|
||||||
@@ -367,7 +375,9 @@ pub struct GroupingConfig {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Grouping Style
|
||||||
pub enum GroupingStyle {
|
pub enum GroupingStyle {
|
||||||
|
/// Default
|
||||||
#[serde(alias = "CtByte")]
|
#[serde(alias = "CtByte")]
|
||||||
Default,
|
Default,
|
||||||
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
|
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
|
||||||
@@ -389,8 +399,9 @@ pub enum GroupingStyle {
|
|||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
/// Rounding configuration
|
||||||
pub enum RoundingConfig {
|
pub enum RoundingConfig {
|
||||||
/// All 4 corners are the same
|
/// All 4 corners are the same
|
||||||
Same(f32),
|
Same(f32),
|
||||||
/// All 4 corners are custom. Order: NW, NE, SW, SE
|
/// All 4 corners are custom. Order: NW, NE, SW, SE
|
||||||
Individual([f32; 4]),
|
Individual([f32; 4]),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use super::ImageIcon;
|
|||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use eframe::egui::vec2;
|
|
||||||
use eframe::egui::Color32;
|
use eframe::egui::Color32;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::CornerRadius;
|
use eframe::egui::CornerRadius;
|
||||||
@@ -17,6 +16,7 @@ use eframe::egui::Stroke;
|
|||||||
use eframe::egui::StrokeKind;
|
use eframe::egui::StrokeKind;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
use eframe::egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
|
use eframe::egui::vec2;
|
||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -34,6 +34,7 @@ const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800);
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Applications widget configuration
|
||||||
pub struct ApplicationsConfig {
|
pub struct ApplicationsConfig {
|
||||||
/// Enables or disables the applications widget.
|
/// Enables or disables the applications widget.
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -44,13 +45,14 @@ pub struct ApplicationsConfig {
|
|||||||
pub spacing: Option<f32>,
|
pub spacing: Option<f32>,
|
||||||
/// Default display format for all applications (optional).
|
/// Default display format for all applications (optional).
|
||||||
/// Could be overridden per application. Defaults to `Icon`.
|
/// Could be overridden per application. Defaults to `Icon`.
|
||||||
pub display: Option<DisplayFormat>,
|
pub display: Option<ApplicationsDisplayFormat>,
|
||||||
/// List of configured applications to display.
|
/// List of configured applications to display.
|
||||||
pub items: Vec<AppConfig>,
|
pub items: Vec<AppConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Application button configuration
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
/// Whether to enable this application button (optional).
|
/// Whether to enable this application button (optional).
|
||||||
/// Inherits from the global `Applications` setting if omitted.
|
/// Inherits from the global `Applications` setting if omitted.
|
||||||
@@ -67,12 +69,13 @@ pub struct AppConfig {
|
|||||||
/// Command to execute (e.g. path to the application or shell command).
|
/// Command to execute (e.g. path to the application or shell command).
|
||||||
pub command: String,
|
pub command: String,
|
||||||
/// Display format for this application button (optional). Overrides global format if set.
|
/// Display format for this application button (optional). Overrides global format if set.
|
||||||
pub display: Option<DisplayFormat>,
|
pub display: Option<ApplicationsDisplayFormat>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub enum DisplayFormat {
|
/// Applications widget display format
|
||||||
|
pub enum ApplicationsDisplayFormat {
|
||||||
/// Show only the application icon.
|
/// Show only the application icon.
|
||||||
#[default]
|
#[default]
|
||||||
Icon,
|
Icon,
|
||||||
@@ -168,7 +171,7 @@ pub struct App {
|
|||||||
/// Command to execute when the application is launched.
|
/// Command to execute when the application is launched.
|
||||||
pub command: UserCommand,
|
pub command: UserCommand,
|
||||||
/// Display format (icon, text, or both).
|
/// Display format (icon, text, or both).
|
||||||
pub display: DisplayFormat,
|
pub display: ApplicationsDisplayFormat,
|
||||||
/// Whether to show the launch command on hover.
|
/// Whether to show the launch command on hover.
|
||||||
pub show_command_on_hover: bool,
|
pub show_command_on_hover: bool,
|
||||||
}
|
}
|
||||||
@@ -183,9 +186,9 @@ impl App {
|
|||||||
ui.spacing_mut().item_spacing = Vec2::splat(4.0);
|
ui.spacing_mut().item_spacing = Vec2::splat(4.0);
|
||||||
|
|
||||||
match self.display {
|
match self.display {
|
||||||
DisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
|
ApplicationsDisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
|
||||||
DisplayFormat::Text => self.draw_name(ui),
|
ApplicationsDisplayFormat::Text => self.draw_name(ui),
|
||||||
DisplayFormat::IconAndText => {
|
ApplicationsDisplayFormat::IconAndText => {
|
||||||
self.draw_icon(ctx, ui, icon_config);
|
self.draw_icon(ctx, ui, icon_config);
|
||||||
self.draw_name(ui);
|
self.draw_name(ui);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,31 @@ use crate::config::LabelPrefix;
|
|||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use eframe::egui::text::LayoutJob;
|
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::Label;
|
use eframe::egui::Label;
|
||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use starship_battery::units::ratio::percent;
|
|
||||||
use starship_battery::Manager;
|
use starship_battery::Manager;
|
||||||
use starship_battery::State;
|
use starship_battery::State;
|
||||||
|
use starship_battery::units::ratio::percent;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Battery widget configuration
|
||||||
pub struct BatteryConfig {
|
pub struct BatteryConfig {
|
||||||
/// Enable the Battery widget
|
/// Enable the Battery widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
/// Hide the widget if the battery is at full charge
|
/// Hide the widget if the battery is at full charge
|
||||||
pub hide_on_full_charge: Option<bool>,
|
pub hide_on_full_charge: Option<bool>,
|
||||||
/// Data refresh interval (default: 10 seconds)
|
/// Data refresh interval in seconds
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||||
pub data_refresh_interval: Option<u64>,
|
pub data_refresh_interval: Option<u64>,
|
||||||
/// Display label prefix
|
/// Display label prefix
|
||||||
pub label_prefix: Option<LabelPrefix>,
|
pub label_prefix: Option<LabelPrefix>,
|
||||||
@@ -87,41 +89,41 @@ impl Battery {
|
|||||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||||
output = None;
|
output = None;
|
||||||
|
|
||||||
if let Ok(mut batteries) = self.manager.batteries() {
|
if let Ok(mut batteries) = self.manager.batteries()
|
||||||
if let Some(Ok(first)) = batteries.nth(0) {
|
&& let Some(Ok(first)) = batteries.nth(0)
|
||||||
let percentage = first.state_of_charge().get::<percent>().round() as u8;
|
{
|
||||||
|
let percentage = first.state_of_charge().get::<percent>().round() as u8;
|
||||||
|
|
||||||
if percentage == 100 && self.hide_on_full_charge {
|
if percentage == 100 && self.hide_on_full_charge {
|
||||||
output = None
|
output = None
|
||||||
} else {
|
} else {
|
||||||
match first.state() {
|
match first.state() {
|
||||||
State::Charging => self.state = BatteryState::Charging,
|
State::Charging => self.state = BatteryState::Charging,
|
||||||
State::Discharging => {
|
State::Discharging => {
|
||||||
self.state = match percentage {
|
self.state = match percentage {
|
||||||
p if p > 75 => BatteryState::Discharging,
|
p if p > 75 => BatteryState::Discharging,
|
||||||
p if p > 50 => BatteryState::High,
|
p if p > 50 => BatteryState::High,
|
||||||
p if p > 25 => BatteryState::Medium,
|
p if p > 25 => BatteryState::Medium,
|
||||||
p if p > 10 => BatteryState::Low,
|
p if p > 10 => BatteryState::Low,
|
||||||
_ => BatteryState::Warning,
|
_ => BatteryState::Warning,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
|
|
||||||
|
|
||||||
output = Some(BatteryOutput {
|
|
||||||
label: match self.label_prefix {
|
|
||||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
|
||||||
format!("BAT: {percentage}%")
|
|
||||||
}
|
|
||||||
LabelPrefix::None | LabelPrefix::Icon => {
|
|
||||||
format!("{percentage}%")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selected,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
|
||||||
|
|
||||||
|
output = Some(BatteryOutput {
|
||||||
|
label: match self.label_prefix {
|
||||||
|
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||||
|
format!("BAT: {percentage}%")
|
||||||
|
}
|
||||||
|
LabelPrefix::None | LabelPrefix::Icon => {
|
||||||
|
format!("{percentage}%")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,13 +178,11 @@ impl BarWidget for Battery {
|
|||||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
&& let Err(error) = Command::new("cmd.exe")
|
||||||
if let Err(error) = Command::new("cmd.exe")
|
|
||||||
.args(["/C", "start", "ms-settings:batterysaver"])
|
.args(["/C", "start", "ms-settings:batterysaver"])
|
||||||
.spawn()
|
.spawn()
|
||||||
{
|
{
|
||||||
eprintln!("{error}")
|
eprintln!("{error}")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
|||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use eframe::egui::text::LayoutJob;
|
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::Label;
|
use eframe::egui::Label;
|
||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -18,10 +18,12 @@ use sysinfo::System;
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// CPU widget configuration
|
||||||
pub struct CpuConfig {
|
pub struct CpuConfig {
|
||||||
/// Enable the Cpu widget
|
/// Enable the Cpu widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
/// Data refresh interval (default: 10 seconds)
|
/// Data refresh interval in seconds
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||||
pub data_refresh_interval: Option<u64>,
|
pub data_refresh_interval: Option<u64>,
|
||||||
/// Display label prefix
|
/// Display label prefix
|
||||||
pub label_prefix: Option<LabelPrefix>,
|
pub label_prefix: Option<LabelPrefix>,
|
||||||
@@ -120,12 +122,10 @@ impl BarWidget for Cpu {
|
|||||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
&& let Err(error) =
|
||||||
if let Err(error) =
|
|
||||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||||
{
|
{
|
||||||
eprintln!("{error}")
|
eprintln!("{error}")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ use crate::selected_frame::SelectableFrame;
|
|||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use eframe::egui::text::LayoutJob;
|
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::Label;
|
use eframe::egui::Label;
|
||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
use eframe::egui::WidgetText;
|
use eframe::egui::WidgetText;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ impl CustomModifiers {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Date widget configuration
|
||||||
pub struct DateConfig {
|
pub struct DateConfig {
|
||||||
/// Enable the Date widget
|
/// Enable the Date widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -103,6 +105,7 @@ impl From<DateConfig> for Date {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Date widget format
|
||||||
pub enum DateFormat {
|
pub enum DateFormat {
|
||||||
/// Month/Date/Year format (09/08/24)
|
/// Month/Date/Year format (09/08/24)
|
||||||
MonthDateYear,
|
MonthDateYear,
|
||||||
@@ -113,8 +116,10 @@ pub enum DateFormat {
|
|||||||
/// Day Date Month Year format (8 September 2024)
|
/// Day Date Month Year format (8 September 2024)
|
||||||
DayDateMonthYear,
|
DayDateMonthYear,
|
||||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||||
Custom(String),
|
Custom(String),
|
||||||
/// Custom format with modifiers
|
/// Custom format with modifiers
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "CustomModifiers"))]
|
||||||
CustomModifiers(CustomModifiers),
|
CustomModifiers(CustomModifiers),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +230,7 @@ impl BarWidget for Date {
|
|||||||
if SelectableFrame::new(false)
|
if SelectableFrame::new(false)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.add(
|
ui.add(
|
||||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
|
||||||
.selectable(false),
|
.selectable(false),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
use crate::config::LabelPrefix;
|
use crate::config::LabelPrefix;
|
||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use eframe::egui::text::LayoutJob;
|
use color_eyre::eyre;
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::Label;
|
use eframe::egui::Label;
|
||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
use eframe::egui::WidgetText;
|
use eframe::egui::WidgetText;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use windows::Win32::Globalization::LCIDToLocaleName;
|
use windows::Win32::Globalization::LCIDToLocaleName;
|
||||||
@@ -19,15 +21,16 @@ use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
|
|||||||
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||||
|
|
||||||
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
|
|
||||||
const ERROR_TEXT: &str = "Error";
|
const ERROR_TEXT: &str = "Error";
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Keyboard widget configuration
|
||||||
pub struct KeyboardConfig {
|
pub struct KeyboardConfig {
|
||||||
/// Enable the Input widget
|
/// Enable the Input widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
/// Data refresh interval (default: 1 second)
|
/// Data refresh interval
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||||
pub data_refresh_interval: Option<u64>,
|
pub data_refresh_interval: Option<u64>,
|
||||||
/// Display label prefix
|
/// Display label prefix
|
||||||
pub label_prefix: Option<LabelPrefix>,
|
pub label_prefix: Option<LabelPrefix>,
|
||||||
@@ -35,9 +38,7 @@ pub struct KeyboardConfig {
|
|||||||
|
|
||||||
impl From<KeyboardConfig> for Keyboard {
|
impl From<KeyboardConfig> for Keyboard {
|
||||||
fn from(value: KeyboardConfig) -> Self {
|
fn from(value: KeyboardConfig) -> Self {
|
||||||
let data_refresh_interval = value
|
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||||
.data_refresh_interval
|
|
||||||
.unwrap_or(DEFAULT_DATA_REFRESH_INTERVAL);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
enable: value.enable,
|
enable: value.enable,
|
||||||
@@ -80,7 +81,7 @@ pub struct Keyboard {
|
|||||||
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
|
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
|
||||||
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
|
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
|
||||||
/// invalid UTF-16 characters during conversion.
|
/// invalid UTF-16 characters during conversion.
|
||||||
fn get_active_keyboard_layout() -> Result<String, ()> {
|
fn get_active_keyboard_layout() -> eyre::Result<String, ()> {
|
||||||
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
|
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
|
||||||
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
|
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
|
||||||
|
|
||||||
@@ -169,7 +170,10 @@ impl BarWidget for Keyboard {
|
|||||||
);
|
);
|
||||||
|
|
||||||
config.apply_on_widget(true, ui, |ui| {
|
config.apply_on_widget(true, ui, |ui| {
|
||||||
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
|
ui.add(
|
||||||
|
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
|
||||||
|
.selectable(false),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use super::ImageIcon;
|
use super::ImageIcon;
|
||||||
|
use crate::MAX_LABEL_WIDTH;
|
||||||
|
use crate::MONITOR_INDEX;
|
||||||
use crate::config::DisplayFormat;
|
use crate::config::DisplayFormat;
|
||||||
use crate::config::DisplayFormat::*;
|
use crate::config::DisplayFormat::*;
|
||||||
use crate::config::WorkspacesDisplayFormat;
|
use crate::config::WorkspacesDisplayFormat;
|
||||||
@@ -7,10 +9,6 @@ use crate::selected_frame::SelectableFrame;
|
|||||||
use crate::ui::CustomUi;
|
use crate::ui::CustomUi;
|
||||||
use crate::widgets::komorebi_layout::KomorebiLayout;
|
use crate::widgets::komorebi_layout::KomorebiLayout;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use crate::MAX_LABEL_WIDTH;
|
|
||||||
use crate::MONITOR_INDEX;
|
|
||||||
use eframe::egui::text::LayoutJob;
|
|
||||||
use eframe::egui::vec2;
|
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Color32;
|
use eframe::egui::Color32;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
@@ -28,6 +26,8 @@ use eframe::egui::StrokeKind;
|
|||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
use eframe::egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
|
use eframe::egui::vec2;
|
||||||
use komorebi_client::Container;
|
use komorebi_client::Container;
|
||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use komorebi_client::Rect;
|
use komorebi_client::Rect;
|
||||||
@@ -49,6 +49,7 @@ use std::sync::atomic::Ordering;
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Komorebi widget configuration
|
||||||
pub struct KomorebiConfig {
|
pub struct KomorebiConfig {
|
||||||
/// Configure the Workspaces widget
|
/// Configure the Workspaces widget
|
||||||
pub workspaces: Option<KomorebiWorkspacesConfig>,
|
pub workspaces: Option<KomorebiWorkspacesConfig>,
|
||||||
@@ -67,6 +68,7 @@ pub struct KomorebiConfig {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Komorebi widget workspaces configuration
|
||||||
pub struct KomorebiWorkspacesConfig {
|
pub struct KomorebiWorkspacesConfig {
|
||||||
/// Enable the Komorebi Workspaces widget
|
/// Enable the Komorebi Workspaces widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -78,6 +80,7 @@ pub struct KomorebiWorkspacesConfig {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Komorebi widget layout configuration
|
||||||
pub struct KomorebiLayoutConfig {
|
pub struct KomorebiLayoutConfig {
|
||||||
/// Enable the Komorebi Layout widget
|
/// Enable the Komorebi Layout widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -89,6 +92,7 @@ pub struct KomorebiLayoutConfig {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Komorebi widget workspace layer configuration
|
||||||
pub struct KomorebiWorkspaceLayerConfig {
|
pub struct KomorebiWorkspaceLayerConfig {
|
||||||
/// Enable the Komorebi Workspace Layer widget
|
/// Enable the Komorebi Workspace Layer widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -100,10 +104,12 @@ pub struct KomorebiWorkspaceLayerConfig {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Komorebi widget focused container configuration
|
||||||
pub struct KomorebiFocusedContainerConfig {
|
pub struct KomorebiFocusedContainerConfig {
|
||||||
/// Enable the Komorebi Focused Container widget
|
/// Enable the Komorebi Focused Container widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused container)
|
/// DEPRECATED: use `display` instead (Show the icon of the currently focused container)
|
||||||
|
#[deprecated(note = "Use `display` instead")]
|
||||||
pub show_icon: Option<bool>,
|
pub show_icon: Option<bool>,
|
||||||
/// Display format of the currently focused container
|
/// Display format of the currently focused container
|
||||||
pub display: Option<DisplayFormat>,
|
pub display: Option<DisplayFormat>,
|
||||||
@@ -111,6 +117,7 @@ pub struct KomorebiFocusedContainerConfig {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Komorebi widget locked container configuration
|
||||||
pub struct KomorebiLockedContainerConfig {
|
pub struct KomorebiLockedContainerConfig {
|
||||||
/// Enable the Komorebi Locked Container widget
|
/// Enable the Komorebi Locked Container widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -122,6 +129,7 @@ pub struct KomorebiLockedContainerConfig {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Komorebi widget configuration switcher configuration
|
||||||
pub struct KomorebiConfigurationSwitcherConfig {
|
pub struct KomorebiConfigurationSwitcherConfig {
|
||||||
/// Enable the Komorebi Configurations widget
|
/// Enable the Komorebi Configurations widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -271,14 +279,14 @@ impl Komorebi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_layout(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
fn render_layout(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||||
if let Some(layout_config) = &self.layout {
|
if let Some(layout_config) = &self.layout
|
||||||
if layout_config.enable {
|
&& layout_config.enable
|
||||||
let monitor_info = &mut *self.monitor_info.borrow_mut();
|
{
|
||||||
let workspace_idx = monitor_info.focused_workspace_idx;
|
let monitor_info = &mut *self.monitor_info.borrow_mut();
|
||||||
monitor_info
|
let workspace_idx = monitor_info.focused_workspace_idx;
|
||||||
.layout
|
monitor_info
|
||||||
.show(ctx, ui, config, layout_config, workspace_idx);
|
.layout
|
||||||
}
|
.show(ctx, ui, config, layout_config, workspace_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,7 +514,9 @@ impl FocusedContainerBar {
|
|||||||
if !value.enable {
|
if !value.enable {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle legacy setting - convert show_icon to display format
|
// Handle legacy setting - convert show_icon to display format
|
||||||
|
#[allow(deprecated)]
|
||||||
let format = value
|
let format = value
|
||||||
.display
|
.display
|
||||||
.unwrap_or(if value.show_icon.unwrap_or(false) {
|
.unwrap_or(if value.show_icon.unwrap_or(false) {
|
||||||
@@ -558,10 +568,8 @@ impl FocusedContainerBar {
|
|||||||
ui.add(img.fit_to_exact_size(self.icon_size));
|
ui.add(img.fit_to_exact_size(self.icon_size));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if HOVEL {
|
if HOVEL && let Some(title) = &info.title {
|
||||||
if let Some(title) = &info.title {
|
inner_response.response.on_hover_text(title);
|
||||||
inner_response.response.on_hover_text(title);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,7 +825,7 @@ impl MonitorInfo {
|
|||||||
self.mouse_follows_focus = state.mouse_follows_focus;
|
self.mouse_follows_focus = state.mouse_follows_focus;
|
||||||
|
|
||||||
let monitor = &state.monitors.elements()[self.monitor_index];
|
let monitor = &state.monitors.elements()[self.monitor_index];
|
||||||
self.work_area_offset = monitor.work_area_offset();
|
self.work_area_offset = monitor.work_area_offset;
|
||||||
self.focused_workspace_idx = Some(monitor.focused_workspace_idx());
|
self.focused_workspace_idx = Some(monitor.focused_workspace_idx());
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
@@ -856,7 +864,7 @@ impl MonitorInfo {
|
|||||||
let containers = fn_containers_from(ws);
|
let containers = fn_containers_from(ws);
|
||||||
WorkspaceInfo {
|
WorkspaceInfo {
|
||||||
name: ws
|
name: ws
|
||||||
.name()
|
.name
|
||||||
.to_owned()
|
.to_owned()
|
||||||
.unwrap_or_else(|| format!("{}", index + 1)),
|
.unwrap_or_else(|| format!("{}", index + 1)),
|
||||||
focused_container_idx: containers.iter().position(|c| c.is_focused),
|
focused_container_idx: containers.iter().position(|c| c.is_focused),
|
||||||
@@ -864,7 +872,7 @@ impl MonitorInfo {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|container| container.windows.iter().any(|window| window.icon.is_some())),
|
.any(|container| container.windows.iter().any(|window| window.icon.is_some())),
|
||||||
containers,
|
containers,
|
||||||
layer: *ws.layer(),
|
layer: ws.layer,
|
||||||
should_show: !hide_empty_ws || focused_ws_idx == Some(index) || !ws.is_empty(),
|
should_show: !hide_empty_ws || focused_ws_idx == Some(index) || !ws.is_empty(),
|
||||||
is_selected: focused_ws_idx == Some(index),
|
is_selected: focused_ws_idx == Some(index),
|
||||||
}
|
}
|
||||||
@@ -873,15 +881,15 @@ impl MonitorInfo {
|
|||||||
|
|
||||||
/// Determines the current layout of the focused workspace
|
/// Determines the current layout of the focused workspace
|
||||||
fn resolve_layout(focused_ws: &Workspace, is_paused: bool) -> KomorebiLayout {
|
fn resolve_layout(focused_ws: &Workspace, is_paused: bool) -> KomorebiLayout {
|
||||||
if focused_ws.monocle_container().is_some() {
|
if focused_ws.monocle_container.is_some() {
|
||||||
KomorebiLayout::Monocle
|
KomorebiLayout::Monocle
|
||||||
} else if !focused_ws.tile() {
|
} else if !focused_ws.tile {
|
||||||
KomorebiLayout::Floating
|
KomorebiLayout::Floating
|
||||||
} else if is_paused {
|
} else if is_paused {
|
||||||
KomorebiLayout::Paused
|
KomorebiLayout::Paused
|
||||||
} else {
|
} else {
|
||||||
match focused_ws.layout() {
|
match focused_ws.layout {
|
||||||
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
|
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(layout),
|
||||||
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
|
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -938,7 +946,7 @@ impl ContainerInfo {
|
|||||||
|
|
||||||
// Monocle container first if present
|
// Monocle container first if present
|
||||||
let monocle = ws
|
let monocle = ws
|
||||||
.monocle_container()
|
.monocle_container
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| Self::from_container(c, !has_focused_float));
|
.map(|c| Self::from_container(c, !has_focused_float));
|
||||||
|
|
||||||
@@ -965,7 +973,7 @@ impl ContainerInfo {
|
|||||||
if let Some(window) = ws.floating_windows().iter().find(|w| w.is_focused()) {
|
if let Some(window) = ws.floating_windows().iter().find(|w| w.is_focused()) {
|
||||||
return Some(Self::from_window(window));
|
return Some(Self::from_window(window));
|
||||||
}
|
}
|
||||||
if let Some(container) = ws.monocle_container() {
|
if let Some(container) = &ws.monocle_container {
|
||||||
Some(Self::from_container(container, true))
|
Some(Self::from_container(container, true))
|
||||||
} else {
|
} else {
|
||||||
ws.focused_container()
|
ws.focused_container()
|
||||||
@@ -979,7 +987,7 @@ impl ContainerInfo {
|
|||||||
windows: container.windows().iter().map(WindowInfo::from).collect(),
|
windows: container.windows().iter().map(WindowInfo::from).collect(),
|
||||||
focused_window_idx: container.focused_window_idx(),
|
focused_window_idx: container.focused_window_idx(),
|
||||||
is_focused,
|
is_focused,
|
||||||
is_locked: container.locked(),
|
is_locked: container.locked,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::config::DisplayFormat;
|
|||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::widgets::komorebi::KomorebiLayoutConfig;
|
use crate::widgets::komorebi::KomorebiLayoutConfig;
|
||||||
use eframe::egui::vec2;
|
use color_eyre::eyre;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::CornerRadius;
|
use eframe::egui::CornerRadius;
|
||||||
use eframe::egui::FontId;
|
use eframe::egui::FontId;
|
||||||
@@ -13,11 +13,12 @@ use eframe::egui::Stroke;
|
|||||||
use eframe::egui::StrokeKind;
|
use eframe::egui::StrokeKind;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
use eframe::egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
|
use eframe::egui::vec2;
|
||||||
use komorebi_client::SocketMessage;
|
use komorebi_client::SocketMessage;
|
||||||
use serde::de::Error;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Deserializer;
|
use serde::Deserializer;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use serde::de::Error;
|
||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::fmt::Formatter;
|
use std::fmt::Formatter;
|
||||||
@@ -25,16 +26,23 @@ use std::fmt::Formatter;
|
|||||||
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
|
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
/// Komorebi layout kind
|
||||||
pub enum KomorebiLayout {
|
pub enum KomorebiLayout {
|
||||||
|
/// Predefined layout
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Default"))]
|
||||||
Default(komorebi_client::DefaultLayout),
|
Default(komorebi_client::DefaultLayout),
|
||||||
|
/// Monocle mode
|
||||||
Monocle,
|
Monocle,
|
||||||
|
/// Floating layer
|
||||||
Floating,
|
Floating,
|
||||||
|
/// Paused
|
||||||
Paused,
|
Paused,
|
||||||
|
/// Custom layout
|
||||||
Custom,
|
Custom,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for KomorebiLayout {
|
impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> eyre::Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
@@ -91,16 +99,15 @@ impl KomorebiLayout {
|
|||||||
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
|
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
|
||||||
match self {
|
match self {
|
||||||
KomorebiLayout::Default(option) => {
|
KomorebiLayout::Default(option) => {
|
||||||
if let Some(ws_idx) = workspace_idx {
|
if let Some(ws_idx) = workspace_idx
|
||||||
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
&& komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
||||||
monitor_idx,
|
monitor_idx,
|
||||||
ws_idx,
|
ws_idx,
|
||||||
*option,
|
*option,
|
||||||
))
|
))
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KomorebiLayout::Monocle => {
|
KomorebiLayout::Monocle => {
|
||||||
@@ -269,57 +276,53 @@ impl KomorebiLayout {
|
|||||||
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
|
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if show_options {
|
if show_options && let Some(workspace_idx) = workspace_idx {
|
||||||
if let Some(workspace_idx) = workspace_idx {
|
Frame::NONE.show(ui, |ui| {
|
||||||
Frame::NONE.show(ui, |ui| {
|
ui.add(
|
||||||
ui.add(
|
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
.selectable(false),
|
||||||
.selectable(false),
|
);
|
||||||
);
|
|
||||||
|
|
||||||
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
||||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
||||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
||||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
||||||
KomorebiLayout::Default(
|
KomorebiLayout::Default(
|
||||||
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
||||||
),
|
),
|
||||||
KomorebiLayout::Default(
|
KomorebiLayout::Default(komorebi_client::DefaultLayout::HorizontalStack),
|
||||||
komorebi_client::DefaultLayout::HorizontalStack,
|
KomorebiLayout::Default(
|
||||||
),
|
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
||||||
KomorebiLayout::Default(
|
),
|
||||||
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
||||||
),
|
//KomorebiLayout::Custom,
|
||||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
KomorebiLayout::Monocle,
|
||||||
//KomorebiLayout::Custom,
|
KomorebiLayout::Floating,
|
||||||
KomorebiLayout::Monocle,
|
KomorebiLayout::Paused,
|
||||||
KomorebiLayout::Floating,
|
]);
|
||||||
KomorebiLayout::Paused,
|
|
||||||
]);
|
|
||||||
|
|
||||||
for layout_option in &mut layout_options {
|
for layout_option in &mut layout_options {
|
||||||
let is_selected = self == layout_option;
|
let is_selected = self == layout_option;
|
||||||
|
|
||||||
if SelectableFrame::new(is_selected)
|
if SelectableFrame::new(is_selected)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
|
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
|
||||||
})
|
})
|
||||||
.on_hover_text(match layout_option {
|
.on_hover_text(match layout_option {
|
||||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||||
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
||||||
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
||||||
KomorebiLayout::Custom => "Custom".to_string(),
|
KomorebiLayout::Custom => "Custom".to_string(),
|
||||||
})
|
})
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
||||||
show_options = false;
|
show_options = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,75 +1,136 @@
|
|||||||
|
use crate::MAX_LABEL_WIDTH;
|
||||||
|
use crate::bar::Alignment;
|
||||||
|
use crate::config::MediaDisplayFormat;
|
||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::ui::CustomUi;
|
use crate::ui::CustomUi;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use crate::MAX_LABEL_WIDTH;
|
|
||||||
use eframe::egui::text::LayoutJob;
|
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::Label;
|
use eframe::egui::Label;
|
||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
use eframe::egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
|
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
|
||||||
|
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Media widget configuration
|
||||||
pub struct MediaConfig {
|
pub struct MediaConfig {
|
||||||
/// Enable the Media widget
|
/// Enable the Media widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
|
/// Display format of the media widget (defaults to IconAndText)
|
||||||
|
pub display: Option<MediaDisplayFormat>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MediaConfig> for Media {
|
impl From<MediaConfig> for Media {
|
||||||
fn from(value: MediaConfig) -> Self {
|
fn from(value: MediaConfig) -> Self {
|
||||||
Self::new(value.enable)
|
Self::new(
|
||||||
|
value.enable,
|
||||||
|
value.display.unwrap_or(MediaDisplayFormat::IconAndText),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Media {
|
pub struct Media {
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
|
pub display: MediaDisplayFormat,
|
||||||
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
|
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Media {
|
impl Media {
|
||||||
pub fn new(enable: bool) -> Self {
|
pub fn new(enable: bool, display: MediaDisplayFormat) -> Self {
|
||||||
Self {
|
Self {
|
||||||
enable,
|
enable,
|
||||||
|
display,
|
||||||
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
|
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get()
|
.join()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle(&self) {
|
pub fn toggle(&self) {
|
||||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
if let Ok(op) = session.TryTogglePlayPauseAsync() {
|
&& let Ok(op) = session.TryTogglePlayPauseAsync()
|
||||||
op.get().unwrap_or_default();
|
{
|
||||||
}
|
op.join().unwrap_or_default();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn previous(&self) {
|
||||||
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
|
&& let Ok(op) = session.TrySkipPreviousAsync()
|
||||||
|
{
|
||||||
|
op.join().unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self) {
|
||||||
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
|
&& let Ok(op) = session.TrySkipNextAsync()
|
||||||
|
{
|
||||||
|
op.join().unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_playing(&self) -> bool {
|
||||||
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
|
&& let Ok(info) = session.GetPlaybackInfo()
|
||||||
|
&& let Ok(status) = info.PlaybackStatus()
|
||||||
|
{
|
||||||
|
return status == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_previous_enabled(&self) -> bool {
|
||||||
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
|
&& let Ok(info) = session.GetPlaybackInfo()
|
||||||
|
&& let Ok(controls) = info.Controls()
|
||||||
|
&& let Ok(enabled) = controls.IsPreviousEnabled()
|
||||||
|
{
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_next_enabled(&self) -> bool {
|
||||||
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
|
&& let Ok(info) = session.GetPlaybackInfo()
|
||||||
|
&& let Ok(controls) = info.Controls()
|
||||||
|
&& let Ok(enabled) = controls.IsNextEnabled()
|
||||||
|
{
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_session(&self) -> bool {
|
||||||
|
self.session_manager.GetCurrentSession().is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
fn output(&mut self) -> String {
|
fn output(&mut self) -> String {
|
||||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||||
if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
|
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
|
||||||
if let Ok(properties) = operation.get() {
|
&& let Ok(properties) = operation.join()
|
||||||
if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
|
&& let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title())
|
||||||
if artist.is_empty() {
|
{
|
||||||
return format!("{title}");
|
if artist.is_empty() {
|
||||||
}
|
return format!("{title}");
|
||||||
|
|
||||||
if title.is_empty() {
|
|
||||||
return format!("{artist}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return format!("{artist} - {title}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if title.is_empty() {
|
||||||
|
return format!("{artist}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return format!("{artist} - {title}");
|
||||||
}
|
}
|
||||||
|
|
||||||
String::new()
|
String::new()
|
||||||
@@ -79,28 +140,96 @@ impl Media {
|
|||||||
impl BarWidget for Media {
|
impl BarWidget for Media {
|
||||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||||
if self.enable {
|
if self.enable {
|
||||||
|
// Don't render if there's no active media session
|
||||||
|
if !self.has_session() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let output = self.output();
|
let output = self.output();
|
||||||
if !output.is_empty() {
|
|
||||||
let mut layout_job = LayoutJob::simple(
|
let show_icon = matches!(
|
||||||
|
self.display,
|
||||||
|
MediaDisplayFormat::Icon
|
||||||
|
| MediaDisplayFormat::IconAndText
|
||||||
|
| MediaDisplayFormat::IconAndControls
|
||||||
|
| MediaDisplayFormat::Full
|
||||||
|
);
|
||||||
|
let show_text = matches!(
|
||||||
|
self.display,
|
||||||
|
MediaDisplayFormat::Text
|
||||||
|
| MediaDisplayFormat::IconAndText
|
||||||
|
| MediaDisplayFormat::TextAndControls
|
||||||
|
| MediaDisplayFormat::Full
|
||||||
|
);
|
||||||
|
let show_controls = matches!(
|
||||||
|
self.display,
|
||||||
|
MediaDisplayFormat::ControlsOnly
|
||||||
|
| MediaDisplayFormat::IconAndControls
|
||||||
|
| MediaDisplayFormat::TextAndControls
|
||||||
|
| MediaDisplayFormat::Full
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't render if there's no media info and we're not showing controls-only
|
||||||
|
if output.is_empty() && !show_controls {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon_font_id = config.icon_font_id.clone();
|
||||||
|
let text_font_id = config.text_font_id.clone();
|
||||||
|
let icon_color = ctx.style().visuals.selection.stroke.color;
|
||||||
|
let text_color = ctx.style().visuals.text_color();
|
||||||
|
|
||||||
|
let mut layout_job = LayoutJob::default();
|
||||||
|
|
||||||
|
if show_icon {
|
||||||
|
layout_job = LayoutJob::simple(
|
||||||
egui_phosphor::regular::HEADPHONES.to_string(),
|
egui_phosphor::regular::HEADPHONES.to_string(),
|
||||||
config.icon_font_id.clone(),
|
icon_font_id.clone(),
|
||||||
ctx.style().visuals.selection.stroke.color,
|
icon_color,
|
||||||
100.0,
|
100.0,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if show_text {
|
||||||
layout_job.append(
|
layout_job.append(
|
||||||
&output,
|
&output,
|
||||||
10.0,
|
if show_icon { 10.0 } else { 0.0 },
|
||||||
TextFormat {
|
TextFormat {
|
||||||
font_id: config.text_font_id.clone(),
|
font_id: text_font_id,
|
||||||
color: ctx.style().visuals.text_color(),
|
color: text_color,
|
||||||
valign: Align::Center,
|
valign: Align::Center,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
config.apply_on_widget(false, ui, |ui| {
|
let is_playing = self.is_playing();
|
||||||
if SelectableFrame::new(false)
|
let is_previous_enabled = self.is_previous_enabled();
|
||||||
|
let is_next_enabled = self.is_next_enabled();
|
||||||
|
let disabled_color = text_color.gamma_multiply(0.5);
|
||||||
|
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
|
||||||
|
|
||||||
|
let prev_color = if is_previous_enabled {
|
||||||
|
text_color
|
||||||
|
} else {
|
||||||
|
disabled_color
|
||||||
|
};
|
||||||
|
|
||||||
|
let next_color = if is_next_enabled {
|
||||||
|
text_color
|
||||||
|
} else {
|
||||||
|
disabled_color
|
||||||
|
};
|
||||||
|
|
||||||
|
let play_pause_icon = if is_playing {
|
||||||
|
egui_phosphor::regular::PAUSE
|
||||||
|
} else {
|
||||||
|
egui_phosphor::regular::PLAY
|
||||||
|
};
|
||||||
|
|
||||||
|
let show_label = |ui: &mut Ui| {
|
||||||
|
if (show_icon || show_text)
|
||||||
|
&& SelectableFrame::new(false)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
let available_height = ui.available_height();
|
let available_height = ui.available_height();
|
||||||
let mut custom_ui = CustomUi(ui);
|
let mut custom_ui = CustomUi(ui);
|
||||||
@@ -110,15 +239,95 @@ impl BarWidget for Media {
|
|||||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||||
available_height,
|
available_height,
|
||||||
),
|
),
|
||||||
Label::new(layout_job).selectable(false).truncate(),
|
Label::new(layout_job.clone()).selectable(false).truncate(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.on_hover_text(&output)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
self.toggle();
|
self.toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let show_previous = |ui: &mut Ui| {
|
||||||
|
if SelectableFrame::new(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
Label::new(LayoutJob::simple(
|
||||||
|
egui_phosphor::regular::SKIP_BACK.to_string(),
|
||||||
|
icon_font_id.clone(),
|
||||||
|
prev_color,
|
||||||
|
100.0,
|
||||||
|
))
|
||||||
|
.selectable(false),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.clicked()
|
||||||
|
&& is_previous_enabled
|
||||||
|
{
|
||||||
|
self.previous();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let show_play_pause = |ui: &mut Ui| {
|
||||||
|
if SelectableFrame::new(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
Label::new(LayoutJob::simple(
|
||||||
|
play_pause_icon.to_string(),
|
||||||
|
icon_font_id.clone(),
|
||||||
|
text_color,
|
||||||
|
100.0,
|
||||||
|
))
|
||||||
|
.selectable(false),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_hover_text(&output)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let show_next = |ui: &mut Ui| {
|
||||||
|
if SelectableFrame::new(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
Label::new(LayoutJob::simple(
|
||||||
|
egui_phosphor::regular::SKIP_FORWARD.to_string(),
|
||||||
|
icon_font_id.clone(),
|
||||||
|
next_color,
|
||||||
|
100.0,
|
||||||
|
))
|
||||||
|
.selectable(false),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.clicked()
|
||||||
|
&& is_next_enabled
|
||||||
|
{
|
||||||
|
self.next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config.apply_on_widget(false, ui, |ui| {
|
||||||
|
if is_reversed {
|
||||||
|
// Right panel renders right-to-left, so reverse order
|
||||||
|
if show_controls {
|
||||||
|
show_next(ui);
|
||||||
|
show_play_pause(ui);
|
||||||
|
show_previous(ui);
|
||||||
}
|
}
|
||||||
});
|
show_label(ui);
|
||||||
}
|
} else {
|
||||||
|
// Left/center panel renders left-to-right, normal order
|
||||||
|
show_label(ui);
|
||||||
|
if show_controls {
|
||||||
|
show_previous(ui);
|
||||||
|
show_play_pause(ui);
|
||||||
|
show_next(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
|||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use eframe::egui::text::LayoutJob;
|
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::Label;
|
use eframe::egui::Label;
|
||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -18,10 +18,12 @@ use sysinfo::System;
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Memory widget configuration
|
||||||
pub struct MemoryConfig {
|
pub struct MemoryConfig {
|
||||||
/// Enable the Memory widget
|
/// Enable the Memory widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
/// Data refresh interval (default: 10 seconds)
|
/// Data refresh interval in seconds
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||||
pub data_refresh_interval: Option<u64>,
|
pub data_refresh_interval: Option<u64>,
|
||||||
/// Display label prefix
|
/// Display label prefix
|
||||||
pub label_prefix: Option<LabelPrefix>,
|
pub label_prefix: Option<LabelPrefix>,
|
||||||
@@ -124,12 +126,10 @@ impl BarWidget for Memory {
|
|||||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
&& let Err(error) =
|
||||||
if let Err(error) =
|
|
||||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||||
{
|
{
|
||||||
eprintln!("{error}")
|
eprintln!("{error}")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub mod media;
|
|||||||
pub mod memory;
|
pub mod memory;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub mod systray;
|
||||||
pub mod time;
|
pub mod time;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
@@ -92,10 +94,16 @@ impl IconsCache {
|
|||||||
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
|
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
|
||||||
self.images.write().unwrap().insert(id, image);
|
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]
|
#[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 size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||||
let pixels = rgba_image.as_flat_samples();
|
let pixels = rgba_image.as_flat_samples();
|
||||||
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
|
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
|
||||||
@@ -156,6 +164,8 @@ pub enum ImageIconId {
|
|||||||
Path(Arc<Path>),
|
Path(Arc<Path>),
|
||||||
/// Windows HWND handle.
|
/// Windows HWND handle.
|
||||||
Hwnd(isize),
|
Hwnd(isize),
|
||||||
|
/// System tray icon identifier.
|
||||||
|
SystrayIcon(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Path> for ImageIconId {
|
impl From<&Path> for ImageIconId {
|
||||||
|
|||||||
@@ -3,24 +3,30 @@ use crate::config::LabelPrefix;
|
|||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use eframe::egui::text::LayoutJob;
|
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Color32;
|
use eframe::egui::Color32;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::Label;
|
use eframe::egui::Label;
|
||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
use num_derive::FromPrimitive;
|
use num_derive::FromPrimitive;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::sync::atomic::AtomicU64;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use sysinfo::Networks;
|
use sysinfo::Networks;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Network widget configuration
|
||||||
pub struct NetworkConfig {
|
pub struct NetworkConfig {
|
||||||
/// Enable the Network widget
|
/// Enable the Network widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -35,7 +41,8 @@ pub struct NetworkConfig {
|
|||||||
/// Characters to reserve for received and transmitted activity
|
/// Characters to reserve for received and transmitted activity
|
||||||
#[serde(alias = "network_activity_fill_characters")]
|
#[serde(alias = "network_activity_fill_characters")]
|
||||||
pub activity_left_padding: Option<usize>,
|
pub activity_left_padding: Option<usize>,
|
||||||
/// Data refresh interval (default: 10 seconds)
|
/// Data refresh interval in seconds
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||||
pub data_refresh_interval: Option<u64>,
|
pub data_refresh_interval: Option<u64>,
|
||||||
/// Display label prefix
|
/// Display label prefix
|
||||||
pub label_prefix: Option<LabelPrefix>,
|
pub label_prefix: Option<LabelPrefix>,
|
||||||
@@ -45,6 +52,7 @@ pub struct NetworkConfig {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Network select configuration
|
||||||
pub struct NetworkSelectConfig {
|
pub struct NetworkSelectConfig {
|
||||||
/// Select the total received data when it's over this value
|
/// Select the total received data when it's over this value
|
||||||
pub total_received_over: Option<u64>,
|
pub total_received_over: Option<u64>,
|
||||||
@@ -58,24 +66,35 @@ pub struct NetworkSelectConfig {
|
|||||||
|
|
||||||
impl From<NetworkConfig> for Network {
|
impl From<NetworkConfig> for Network {
|
||||||
fn from(value: NetworkConfig) -> Self {
|
fn from(value: NetworkConfig) -> Self {
|
||||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
let default_refresh_interval = 10;
|
||||||
|
let data_refresh_interval = value
|
||||||
|
.data_refresh_interval
|
||||||
|
.unwrap_or(default_refresh_interval);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
enable: value.enable,
|
enable: value.enable,
|
||||||
show_total_activity: value.show_total_activity,
|
show_total_activity: value.show_total_activity,
|
||||||
show_activity: value.show_activity,
|
show_activity: value.show_activity,
|
||||||
show_default_interface: value.show_default_interface.unwrap_or(true),
|
show_default_interface: value.show_default_interface.unwrap_or(true),
|
||||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
networks_network_activity: Arc::new(Mutex::new(Networks::new_with_refreshed_list())),
|
||||||
default_interface: String::new(),
|
default_interface: Arc::new(Mutex::new(String::new())),
|
||||||
|
interface_generation: Arc::new(AtomicU64::new(0)),
|
||||||
|
default_refresh_interval,
|
||||||
data_refresh_interval,
|
data_refresh_interval,
|
||||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||||
auto_select: value.auto_select,
|
auto_select: value.auto_select,
|
||||||
activity_left_padding: value.activity_left_padding.unwrap_or_default(),
|
activity_left_padding: value.activity_left_padding.unwrap_or_default(),
|
||||||
last_state_total_activity: vec![],
|
last_update_request_default_interface: Instant::now()
|
||||||
last_state_activity: vec![],
|
.checked_sub(Duration::from_secs(default_refresh_interval))
|
||||||
last_updated_network_activity: Instant::now()
|
|
||||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
last_state_total_activity: Arc::new(Mutex::new(vec![])),
|
||||||
|
last_state_activity: Arc::new(Mutex::new(vec![])),
|
||||||
|
last_update_request_network_activity: Arc::new(Mutex::new(
|
||||||
|
Instant::now()
|
||||||
|
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||||
|
.unwrap(),
|
||||||
|
)),
|
||||||
|
activity_generation: Arc::new(AtomicU64::new(0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,84 +104,139 @@ pub struct Network {
|
|||||||
pub show_total_activity: bool,
|
pub show_total_activity: bool,
|
||||||
pub show_activity: bool,
|
pub show_activity: bool,
|
||||||
pub show_default_interface: bool,
|
pub show_default_interface: bool,
|
||||||
networks_network_activity: Networks,
|
networks_network_activity: Arc<Mutex<Networks>>,
|
||||||
|
default_refresh_interval: u64,
|
||||||
data_refresh_interval: u64,
|
data_refresh_interval: u64,
|
||||||
label_prefix: LabelPrefix,
|
label_prefix: LabelPrefix,
|
||||||
auto_select: Option<NetworkSelectConfig>,
|
auto_select: Option<NetworkSelectConfig>,
|
||||||
default_interface: String,
|
default_interface: Arc<Mutex<String>>,
|
||||||
last_state_total_activity: Vec<NetworkReading>,
|
interface_generation: Arc<AtomicU64>,
|
||||||
last_state_activity: Vec<NetworkReading>,
|
last_update_request_default_interface: Instant,
|
||||||
last_updated_network_activity: Instant,
|
activity_generation: Arc<AtomicU64>,
|
||||||
|
last_state_activity: Arc<Mutex<Vec<NetworkReading>>>,
|
||||||
|
last_state_total_activity: Arc<Mutex<Vec<NetworkReading>>>,
|
||||||
|
last_update_request_network_activity: Arc<Mutex<Instant>>,
|
||||||
activity_left_padding: usize,
|
activity_left_padding: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Network {
|
impl Network {
|
||||||
fn default_interface(&mut self) {
|
fn update_default_interface_async(&mut self) {
|
||||||
if let Ok(interface) = netdev::get_default_interface() {
|
let gen_ = self.interface_generation.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
if let Some(friendly_name) = &interface.friendly_name {
|
let gen_arc = Arc::clone(&self.interface_generation);
|
||||||
self.default_interface.clone_from(friendly_name);
|
let iface_arc = Arc::clone(&self.default_interface);
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
if let Ok(interface) = netdev::get_default_interface()
|
||||||
|
&& let Some(friendly_name) = &interface.friendly_name
|
||||||
|
{
|
||||||
|
// Only update if this is the latest request
|
||||||
|
if gen_ == gen_arc.load(Ordering::SeqCst)
|
||||||
|
&& let Ok(mut iface) = iface_arc.lock()
|
||||||
|
{
|
||||||
|
*iface = friendly_name.clone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
fn default_interface(&mut self) -> String {
|
||||||
let mut activity = self.last_state_activity.clone();
|
let current = self.default_interface.lock().unwrap().clone();
|
||||||
let mut total_activity = self.last_state_total_activity.clone();
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
if now.duration_since(self.last_updated_network_activity)
|
if now.duration_since(self.last_update_request_default_interface)
|
||||||
> Duration::from_secs(self.data_refresh_interval)
|
> Duration::from_secs(self.default_refresh_interval)
|
||||||
{
|
{
|
||||||
activity.clear();
|
self.last_update_request_default_interface = now;
|
||||||
total_activity.clear();
|
self.update_default_interface_async();
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(interface) = netdev::get_default_interface() {
|
current
|
||||||
if let Some(friendly_name) = &interface.friendly_name {
|
}
|
||||||
self.default_interface.clone_from(friendly_name);
|
|
||||||
|
|
||||||
self.networks_network_activity.refresh(true);
|
fn update_network_activity_async(&mut self) {
|
||||||
|
let gen_ = self.activity_generation.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|
let gen_arc = Arc::clone(&self.activity_generation);
|
||||||
|
let activity_arc = Arc::clone(&self.last_state_activity);
|
||||||
|
let total_activity_arc = Arc::clone(&self.last_state_total_activity);
|
||||||
|
let data_refresh_interval = self.data_refresh_interval;
|
||||||
|
let show_activity = self.show_activity;
|
||||||
|
let show_total_activity = self.show_total_activity;
|
||||||
|
let networks_network_activity_arc = Arc::clone(&self.networks_network_activity);
|
||||||
|
|
||||||
for (interface_name, data) in &self.networks_network_activity {
|
thread::spawn(move || {
|
||||||
if friendly_name.eq(interface_name) {
|
let mut activity = Vec::new();
|
||||||
if self.show_activity {
|
let mut total_activity = Vec::new();
|
||||||
let received = Self::to_pretty_bytes(
|
|
||||||
data.received(),
|
|
||||||
self.data_refresh_interval,
|
|
||||||
);
|
|
||||||
let transmitted = Self::to_pretty_bytes(
|
|
||||||
data.transmitted(),
|
|
||||||
self.data_refresh_interval,
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.push(NetworkReading::new(
|
if let Ok(interface) = netdev::get_default_interface()
|
||||||
NetworkReadingFormat::Speed,
|
&& let Some(friendly_name) = &interface.friendly_name
|
||||||
ReadingValue::from(received),
|
&& let Ok(mut networks) = networks_network_activity_arc.lock()
|
||||||
ReadingValue::from(transmitted),
|
{
|
||||||
));
|
networks.refresh(true);
|
||||||
}
|
|
||||||
|
|
||||||
if self.show_total_activity {
|
for (interface_name, data) in &*networks {
|
||||||
let total_received =
|
if friendly_name.eq(interface_name) {
|
||||||
Self::to_pretty_bytes(data.total_received(), 1);
|
if show_activity {
|
||||||
let total_transmitted =
|
let received =
|
||||||
Self::to_pretty_bytes(data.total_transmitted(), 1);
|
Network::to_pretty_bytes(data.received(), data_refresh_interval);
|
||||||
|
let transmitted =
|
||||||
|
Network::to_pretty_bytes(data.transmitted(), data_refresh_interval);
|
||||||
|
|
||||||
total_activity.push(NetworkReading::new(
|
activity.push(NetworkReading::new(
|
||||||
NetworkReadingFormat::Total,
|
NetworkReadingFormat::Speed,
|
||||||
ReadingValue::from(total_received),
|
ReadingValue::from(received),
|
||||||
ReadingValue::from(total_transmitted),
|
ReadingValue::from(transmitted),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if show_total_activity {
|
||||||
|
let total_received = Network::to_pretty_bytes(data.total_received(), 1);
|
||||||
|
let total_transmitted =
|
||||||
|
Network::to_pretty_bytes(data.total_transmitted(), 1);
|
||||||
|
|
||||||
|
total_activity.push(NetworkReading::new(
|
||||||
|
NetworkReadingFormat::Total,
|
||||||
|
ReadingValue::from(total_received),
|
||||||
|
ReadingValue::from(total_transmitted),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_state_activity.clone_from(&activity);
|
// Only update if this is the latest request
|
||||||
self.last_state_total_activity.clone_from(&total_activity);
|
if gen_ == gen_arc.load(Ordering::SeqCst) {
|
||||||
self.last_updated_network_activity = now;
|
if let Ok(mut act) = activity_arc.lock() {
|
||||||
|
*act = activity;
|
||||||
|
}
|
||||||
|
if let Ok(mut tot) = total_activity_arc.lock() {
|
||||||
|
*tot = total_activity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||||
|
let now = Instant::now();
|
||||||
|
let should_update = {
|
||||||
|
let last_update_request = self.last_update_request_network_activity.lock().unwrap();
|
||||||
|
now.duration_since(*last_update_request)
|
||||||
|
> Duration::from_secs(self.data_refresh_interval)
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_update {
|
||||||
|
{
|
||||||
|
let mut last_updated = self.last_update_request_network_activity.lock().unwrap();
|
||||||
|
*last_updated = now;
|
||||||
|
}
|
||||||
|
self.update_network_activity_async();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.get_network_activity()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_network_activity(&self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||||
|
let activity = self.last_state_activity.lock().unwrap().clone();
|
||||||
|
let total_activity = self.last_state_total_activity.lock().unwrap().clone();
|
||||||
(activity, total_activity)
|
(activity, total_activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,10 +386,9 @@ impl Network {
|
|||||||
if SelectableFrame::new_auto(selected, auto_focus_fill)
|
if SelectableFrame::new_auto(selected, auto_focus_fill)
|
||||||
.show(ui, add_contents)
|
.show(ui, add_contents)
|
||||||
.clicked()
|
.clicked()
|
||||||
|
&& let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
|
||||||
{
|
{
|
||||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
eprintln!("{error}");
|
||||||
eprintln!("{error}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,9 +507,9 @@ impl BarWidget for Network {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.show_default_interface {
|
if self.show_default_interface {
|
||||||
self.default_interface();
|
let mut self_default_interface = self.default_interface();
|
||||||
|
|
||||||
if !self.default_interface.is_empty() {
|
if !self_default_interface.is_empty() {
|
||||||
let mut layout_job = LayoutJob::simple(
|
let mut layout_job = LayoutJob::simple(
|
||||||
match self.label_prefix {
|
match self.label_prefix {
|
||||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||||
@@ -450,11 +523,11 @@ impl BarWidget for Network {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||||
self.default_interface.insert_str(0, "NET: ");
|
self_default_interface.insert_str(0, "NET: ");
|
||||||
}
|
}
|
||||||
|
|
||||||
layout_job.append(
|
layout_job.append(
|
||||||
&self.default_interface,
|
&self_default_interface,
|
||||||
10.0,
|
10.0,
|
||||||
TextFormat {
|
TextFormat {
|
||||||
font_id: config.text_font_id.clone(),
|
font_id: config.text_font_id.clone(),
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ use crate::config::LabelPrefix;
|
|||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use eframe::egui::text::LayoutJob;
|
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::Label;
|
use eframe::egui::Label;
|
||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -18,17 +18,23 @@ use sysinfo::Disks;
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Storage widget configuration
|
||||||
pub struct StorageConfig {
|
pub struct StorageConfig {
|
||||||
/// Enable the Storage widget
|
/// Enable the Storage widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
/// Data refresh interval (default: 10 seconds)
|
/// Data refresh interval in seconds
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||||
pub data_refresh_interval: Option<u64>,
|
pub data_refresh_interval: Option<u64>,
|
||||||
/// Display label prefix
|
/// Display label prefix
|
||||||
pub label_prefix: Option<LabelPrefix>,
|
pub label_prefix: Option<LabelPrefix>,
|
||||||
/// Show disks that are read only. (default: false)
|
/// Show disks that are read only
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
|
||||||
pub show_read_only_disks: Option<bool>,
|
pub show_read_only_disks: Option<bool>,
|
||||||
/// Show removable disks. (default: true)
|
/// Show removable disks
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||||
pub show_removable_disks: Option<bool>,
|
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]]
|
/// Select when the current percentage is over this value [[1-100]]
|
||||||
pub auto_select_over: Option<u8>,
|
pub auto_select_over: Option<u8>,
|
||||||
/// Hide when the current percentage is under this value [[1-100]]
|
/// Hide when the current percentage is under this value [[1-100]]
|
||||||
@@ -44,6 +50,9 @@ impl From<StorageConfig> for Storage {
|
|||||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||||
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
|
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
|
||||||
show_removable_disks: value.show_removable_disks.unwrap_or(true),
|
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_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)),
|
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
||||||
last_updated: Instant::now(),
|
last_updated: Instant::now(),
|
||||||
@@ -51,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 {
|
struct StorageDisk {
|
||||||
label: String,
|
label: String,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
@@ -63,6 +85,7 @@ pub struct Storage {
|
|||||||
label_prefix: LabelPrefix,
|
label_prefix: LabelPrefix,
|
||||||
show_read_only_disks: bool,
|
show_read_only_disks: bool,
|
||||||
show_removable_disks: bool,
|
show_removable_disks: bool,
|
||||||
|
storage_display_name: StorageDisplayName,
|
||||||
auto_select_over: Option<u8>,
|
auto_select_over: Option<u8>,
|
||||||
auto_hide_under: Option<u8>,
|
auto_hide_under: Option<u8>,
|
||||||
last_updated: Instant,
|
last_updated: Instant,
|
||||||
@@ -86,6 +109,17 @@ impl Storage {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mount = disk.mount_point();
|
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 total = disk.total_space();
|
||||||
let available = disk.available_space();
|
let available = disk.available_space();
|
||||||
let used = total - available;
|
let used = total - available;
|
||||||
@@ -99,7 +133,7 @@ impl Storage {
|
|||||||
disks.push(StorageDisk {
|
disks.push(StorageDisk {
|
||||||
label: match self.label_prefix {
|
label: match self.label_prefix {
|
||||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||||
format!("{} {}%", mount.to_string_lossy(), percentage)
|
format!("{} {}%", display_name, percentage)
|
||||||
}
|
}
|
||||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
|
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
|
||||||
},
|
},
|
||||||
@@ -156,17 +190,15 @@ impl BarWidget for Storage {
|
|||||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
&& let Err(error) = Command::new("cmd.exe")
|
||||||
if let Err(error) = Command::new("cmd.exe")
|
|
||||||
.args([
|
.args([
|
||||||
"/C",
|
"/C",
|
||||||
"explorer.exe",
|
"explorer.exe",
|
||||||
output.label.split(' ').collect::<Vec<&str>>()[0],
|
output.label.split(' ').collect::<Vec<&str>>()[0],
|
||||||
])
|
])
|
||||||
.spawn()
|
.spawn()
|
||||||
{
|
{
|
||||||
eprintln!("{error}")
|
eprintln!("{error}")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ use crate::widgets::widget::BarWidget;
|
|||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use chrono::NaiveTime;
|
use chrono::NaiveTime;
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use eframe::egui::text::LayoutJob;
|
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::CornerRadius;
|
use eframe::egui::CornerRadius;
|
||||||
@@ -16,6 +15,7 @@ use eframe::egui::Stroke;
|
|||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
use eframe::egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
use eframe::epaint::StrokeKind;
|
use eframe::epaint::StrokeKind;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -72,6 +72,7 @@ lazy_static! {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Time widget configuration
|
||||||
pub struct TimeConfig {
|
pub struct TimeConfig {
|
||||||
/// Enable the Time widget
|
/// Enable the Time widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
@@ -92,7 +93,7 @@ pub struct TimeConfig {
|
|||||||
///}
|
///}
|
||||||
/// ```
|
/// ```
|
||||||
pub timezone: Option<String>,
|
pub timezone: Option<String>,
|
||||||
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00. (default: false)
|
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00
|
||||||
pub changing_icon: Option<bool>,
|
pub changing_icon: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ impl From<TimeConfig> for Time {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Time format
|
||||||
pub enum TimeFormat {
|
pub enum TimeFormat {
|
||||||
/// Twelve-hour format (with seconds)
|
/// Twelve-hour format (with seconds)
|
||||||
TwelveHour,
|
TwelveHour,
|
||||||
@@ -133,6 +135,7 @@ pub enum TimeFormat {
|
|||||||
/// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
|
/// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
|
||||||
BinaryRectangle,
|
BinaryRectangle,
|
||||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||||
Custom(String),
|
Custom(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
|||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use eframe::egui::text::LayoutJob;
|
|
||||||
use eframe::egui::Align;
|
use eframe::egui::Align;
|
||||||
use eframe::egui::Context;
|
use eframe::egui::Context;
|
||||||
use eframe::egui::Label;
|
use eframe::egui::Label;
|
||||||
use eframe::egui::TextFormat;
|
use eframe::egui::TextFormat;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
|
use eframe::egui::text::LayoutJob;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -16,10 +16,12 @@ use std::time::Instant;
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Update widget configuration
|
||||||
pub struct UpdateConfig {
|
pub struct UpdateConfig {
|
||||||
/// Enable the Update widget
|
/// Enable the Update widget
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
/// Data refresh interval (default: 12 hours)
|
/// Data refresh interval in hours
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12)))]
|
||||||
pub data_refresh_interval: Option<u64>,
|
pub data_refresh_interval: Option<u64>,
|
||||||
/// Display label prefix
|
/// Display label prefix
|
||||||
pub label_prefix: Option<LabelPrefix>,
|
pub label_prefix: Option<LabelPrefix>,
|
||||||
@@ -140,16 +142,14 @@ impl BarWidget for Update {
|
|||||||
if SelectableFrame::new(false)
|
if SelectableFrame::new(false)
|
||||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
&& let Err(error) = Command::new("explorer.exe")
|
||||||
if let Err(error) = Command::new("explorer.exe")
|
|
||||||
.args([format!(
|
.args([format!(
|
||||||
"https://github.com/LGUG2Z/komorebi/releases/v{}",
|
"https://github.com/LGUG2Z/komorebi/releases/v{}",
|
||||||
self.latest_version
|
self.latest_version
|
||||||
)])
|
)])
|
||||||
.spawn()
|
.spawn()
|
||||||
{
|
{
|
||||||
eprintln!("{error}")
|
eprintln!("{error}")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ use crate::widgets::network::Network;
|
|||||||
use crate::widgets::network::NetworkConfig;
|
use crate::widgets::network::NetworkConfig;
|
||||||
use crate::widgets::storage::Storage;
|
use crate::widgets::storage::Storage;
|
||||||
use crate::widgets::storage::StorageConfig;
|
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::Time;
|
||||||
use crate::widgets::time::TimeConfig;
|
use crate::widgets::time::TimeConfig;
|
||||||
use crate::widgets::update::Update;
|
use crate::widgets::update::Update;
|
||||||
@@ -34,18 +38,47 @@ pub trait BarWidget {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Widget configuration
|
||||||
pub enum WidgetConfig {
|
pub enum WidgetConfig {
|
||||||
|
/// Applications widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Applications"))]
|
||||||
Applications(ApplicationsConfig),
|
Applications(ApplicationsConfig),
|
||||||
|
/// Battery widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Battery"))]
|
||||||
Battery(BatteryConfig),
|
Battery(BatteryConfig),
|
||||||
|
/// CPU widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Cpu"))]
|
||||||
Cpu(CpuConfig),
|
Cpu(CpuConfig),
|
||||||
|
/// Date widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Date"))]
|
||||||
Date(DateConfig),
|
Date(DateConfig),
|
||||||
|
/// Keyboard widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Keyboard"))]
|
||||||
Keyboard(KeyboardConfig),
|
Keyboard(KeyboardConfig),
|
||||||
|
/// Komorebi widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Komorebi"))]
|
||||||
Komorebi(KomorebiConfig),
|
Komorebi(KomorebiConfig),
|
||||||
|
/// Media widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Media"))]
|
||||||
Media(MediaConfig),
|
Media(MediaConfig),
|
||||||
|
/// Memory widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Memory"))]
|
||||||
Memory(MemoryConfig),
|
Memory(MemoryConfig),
|
||||||
|
/// Network widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Network"))]
|
||||||
Network(NetworkConfig),
|
Network(NetworkConfig),
|
||||||
|
/// Storage widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
|
||||||
Storage(StorageConfig),
|
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),
|
Time(TimeConfig),
|
||||||
|
/// Update widget configuration
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Update"))]
|
||||||
Update(UpdateConfig),
|
Update(UpdateConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +95,8 @@ impl WidgetConfig {
|
|||||||
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
||||||
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
|
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
|
||||||
WidgetConfig::Storage(config) => Box::new(Storage::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::Time(config) => Box::new(Time::from(config.clone())),
|
||||||
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
|
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
|
||||||
}
|
}
|
||||||
@@ -87,6 +122,8 @@ impl WidgetConfig {
|
|||||||
WidgetConfig::Memory(config) => config.enable,
|
WidgetConfig::Memory(config) => config.enable,
|
||||||
WidgetConfig::Network(config) => config.enable,
|
WidgetConfig::Network(config) => config.enable,
|
||||||
WidgetConfig::Storage(config) => config.enable,
|
WidgetConfig::Storage(config) => config.enable,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
WidgetConfig::Systray(config) => config.enable,
|
||||||
WidgetConfig::Time(config) => config.enable,
|
WidgetConfig::Time(config) => config.enable,
|
||||||
WidgetConfig::Update(config) => config.enable,
|
WidgetConfig::Update(config) => config.enable,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-client"
|
name = "komorebi-client"
|
||||||
version = "0.1.38"
|
version = "0.1.41"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|||||||
+34
-26
@@ -1,8 +1,36 @@
|
|||||||
#![warn(clippy::all)]
|
#![warn(clippy::all)]
|
||||||
#![allow(clippy::missing_errors_doc)]
|
#![allow(clippy::missing_errors_doc)]
|
||||||
|
|
||||||
pub use komorebi::animation::prefix::AnimationPrefix;
|
pub use komorebi::AnimationsConfig;
|
||||||
|
pub use komorebi::AppSpecificConfigurationPath;
|
||||||
|
pub use komorebi::AspectRatio;
|
||||||
|
pub use komorebi::BorderColours;
|
||||||
|
pub use komorebi::Colour;
|
||||||
|
pub use komorebi::CrossBoundaryBehaviour;
|
||||||
|
pub use komorebi::GridLayoutOptions;
|
||||||
|
pub use komorebi::KomorebiTheme;
|
||||||
|
pub use komorebi::LayoutOptions;
|
||||||
|
pub use komorebi::MonitorConfig;
|
||||||
|
pub use komorebi::Notification;
|
||||||
|
pub use komorebi::NotificationEvent;
|
||||||
|
pub use komorebi::Placement;
|
||||||
|
pub use komorebi::PredefinedAspectRatio;
|
||||||
|
pub use komorebi::Rgb;
|
||||||
|
pub use komorebi::RuleDebug;
|
||||||
|
pub use komorebi::ScrollingLayoutOptions;
|
||||||
|
pub use komorebi::StackbarConfig;
|
||||||
|
pub use komorebi::StaticConfig;
|
||||||
|
pub use komorebi::SubscribeOptions;
|
||||||
|
pub use komorebi::TabsConfig;
|
||||||
|
pub use komorebi::ThemeOptions;
|
||||||
|
pub use komorebi::VirtualDesktopNotification;
|
||||||
|
pub use komorebi::Wallpaper;
|
||||||
|
pub use komorebi::WindowContainerBehaviour;
|
||||||
|
pub use komorebi::WindowHandlingBehaviour;
|
||||||
|
pub use komorebi::WindowsApi;
|
||||||
|
pub use komorebi::WorkspaceConfig;
|
||||||
pub use komorebi::animation::PerAnimationPrefixConfig;
|
pub use komorebi::animation::PerAnimationPrefixConfig;
|
||||||
|
pub use komorebi::animation::prefix::AnimationPrefix;
|
||||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||||
pub use komorebi::border_manager::BorderInfo;
|
pub use komorebi::border_manager::BorderInfo;
|
||||||
pub use komorebi::config_generation::ApplicationConfiguration;
|
pub use komorebi::config_generation::ApplicationConfiguration;
|
||||||
@@ -11,8 +39,6 @@ pub use komorebi::config_generation::IdWithIdentifierAndComment;
|
|||||||
pub use komorebi::config_generation::MatchingRule;
|
pub use komorebi::config_generation::MatchingRule;
|
||||||
pub use komorebi::config_generation::MatchingStrategy;
|
pub use komorebi::config_generation::MatchingStrategy;
|
||||||
pub use komorebi::container::Container;
|
pub use komorebi::container::Container;
|
||||||
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
|
||||||
pub use komorebi::core::replace_env_in_path;
|
|
||||||
pub use komorebi::core::AnimationStyle;
|
pub use komorebi::core::AnimationStyle;
|
||||||
pub use komorebi::core::ApplicationIdentifier;
|
pub use komorebi::core::ApplicationIdentifier;
|
||||||
pub use komorebi::core::Arrangement;
|
pub use komorebi::core::Arrangement;
|
||||||
@@ -42,38 +68,20 @@ pub use komorebi::core::StackbarLabel;
|
|||||||
pub use komorebi::core::StackbarMode;
|
pub use komorebi::core::StackbarMode;
|
||||||
pub use komorebi::core::StateQuery;
|
pub use komorebi::core::StateQuery;
|
||||||
pub use komorebi::core::WindowKind;
|
pub use komorebi::core::WindowKind;
|
||||||
|
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
||||||
|
pub use komorebi::core::replace_env_in_path;
|
||||||
pub use komorebi::monitor::Monitor;
|
pub use komorebi::monitor::Monitor;
|
||||||
pub use komorebi::monitor_reconciliator::MonitorNotification;
|
pub use komorebi::monitor_reconciliator::MonitorNotification;
|
||||||
pub use komorebi::ring::Ring;
|
pub use komorebi::ring::Ring;
|
||||||
|
pub use komorebi::splash;
|
||||||
|
pub use komorebi::state::GlobalState;
|
||||||
|
pub use komorebi::state::State;
|
||||||
pub use komorebi::win32_display_data;
|
pub use komorebi::win32_display_data;
|
||||||
pub use komorebi::window::Window;
|
pub use komorebi::window::Window;
|
||||||
pub use komorebi::window_manager_event::WindowManagerEvent;
|
pub use komorebi::window_manager_event::WindowManagerEvent;
|
||||||
pub use komorebi::workspace::Workspace;
|
pub use komorebi::workspace::Workspace;
|
||||||
pub use komorebi::workspace::WorkspaceGlobals;
|
pub use komorebi::workspace::WorkspaceGlobals;
|
||||||
pub use komorebi::workspace::WorkspaceLayer;
|
pub use komorebi::workspace::WorkspaceLayer;
|
||||||
pub use komorebi::AnimationsConfig;
|
|
||||||
pub use komorebi::AppSpecificConfigurationPath;
|
|
||||||
pub use komorebi::AspectRatio;
|
|
||||||
pub use komorebi::BorderColours;
|
|
||||||
pub use komorebi::Colour;
|
|
||||||
pub use komorebi::CrossBoundaryBehaviour;
|
|
||||||
pub use komorebi::GlobalState;
|
|
||||||
pub use komorebi::KomorebiTheme;
|
|
||||||
pub use komorebi::MonitorConfig;
|
|
||||||
pub use komorebi::Notification;
|
|
||||||
pub use komorebi::NotificationEvent;
|
|
||||||
pub use komorebi::PredefinedAspectRatio;
|
|
||||||
pub use komorebi::Rgb;
|
|
||||||
pub use komorebi::RuleDebug;
|
|
||||||
pub use komorebi::StackbarConfig;
|
|
||||||
pub use komorebi::State;
|
|
||||||
pub use komorebi::StaticConfig;
|
|
||||||
pub use komorebi::SubscribeOptions;
|
|
||||||
pub use komorebi::TabsConfig;
|
|
||||||
pub use komorebi::VirtualDesktopNotification;
|
|
||||||
pub use komorebi::WindowContainerBehaviour;
|
|
||||||
pub use komorebi::WindowsApi;
|
|
||||||
pub use komorebi::WorkspaceConfig;
|
|
||||||
|
|
||||||
use komorebi::DATA_DIR;
|
use komorebi::DATA_DIR;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-gui"
|
name = "komorebi-gui"
|
||||||
version = "0.1.38"
|
version = "0.1.41"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -13,4 +13,4 @@ egui_extras = { workspace = true }
|
|||||||
random_word = { version = "0.5", features = ["en"] }
|
random_word = { version = "0.5", features = ["en"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
windows-core = { workspace = true }
|
windows-core = { workspace = true }
|
||||||
windows = { workspace = true }
|
windows = { workspace = true }
|
||||||
|
|||||||
+14
-14
@@ -1,9 +1,9 @@
|
|||||||
#![warn(clippy::all)]
|
#![warn(clippy::all)]
|
||||||
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use eframe::egui::color_picker::Alpha;
|
|
||||||
use eframe::egui::Color32;
|
use eframe::egui::Color32;
|
||||||
use eframe::egui::ViewportBuilder;
|
use eframe::egui::ViewportBuilder;
|
||||||
|
use eframe::egui::color_picker::Alpha;
|
||||||
use komorebi_client::BorderStyle;
|
use komorebi_client::BorderStyle;
|
||||||
use komorebi_client::Colour;
|
use komorebi_client::Colour;
|
||||||
use komorebi_client::DefaultLayout;
|
use komorebi_client::DefaultLayout;
|
||||||
@@ -78,8 +78,8 @@ impl From<&komorebi_client::Monitor> for MonitorConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
size: *value.size(),
|
size: value.size,
|
||||||
work_area_offset: value.work_area_offset().unwrap_or_default(),
|
work_area_offset: value.work_area_offset.unwrap_or_default(),
|
||||||
workspaces,
|
workspaces,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,22 +95,22 @@ struct WorkspaceConfig {
|
|||||||
|
|
||||||
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
|
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
|
||||||
fn from(value: &komorebi_client::Workspace) -> Self {
|
fn from(value: &komorebi_client::Workspace) -> Self {
|
||||||
let layout = match value.layout() {
|
let layout = match value.layout {
|
||||||
Layout::Default(layout) => *layout,
|
Layout::Default(layout) => layout,
|
||||||
Layout::Custom(_) => DefaultLayout::BSP,
|
Layout::Custom(_) => DefaultLayout::BSP,
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = value
|
let name = value
|
||||||
.name()
|
.name
|
||||||
.to_owned()
|
.to_owned()
|
||||||
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
|
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
layout,
|
layout,
|
||||||
name,
|
name,
|
||||||
tile: *value.tile(),
|
tile: value.tile,
|
||||||
workspace_padding: value.workspace_padding().unwrap_or(20),
|
workspace_padding: value.workspace_padding.unwrap_or(20),
|
||||||
container_padding: value.container_padding().unwrap_or(20),
|
container_padding: value.container_padding.unwrap_or(20),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,7 +247,7 @@ impl eframe::App for KomorebiGui {
|
|||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
ctx.set_pixels_per_point(2.0);
|
ctx.set_pixels_per_point(2.0);
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
ui.set_width(ctx.screen_rect().width());
|
ui.set_width(ctx.content_rect().width());
|
||||||
ui.collapsing("Debugging", |ui| {
|
ui.collapsing("Debugging", |ui| {
|
||||||
ui.collapsing("Window Rules", |ui| {
|
ui.collapsing("Window Rules", |ui| {
|
||||||
let window = Window::from(self.debug_hwnd);
|
let window = Window::from(self.debug_hwnd);
|
||||||
@@ -437,7 +437,7 @@ impl eframe::App for KomorebiGui {
|
|||||||
BorderStyle::Square,
|
BorderStyle::Square,
|
||||||
] {
|
] {
|
||||||
if ui
|
if ui
|
||||||
.add(egui::SelectableLabel::new(
|
.add(egui::Button::selectable(
|
||||||
self.border_config.border_style == option,
|
self.border_config.border_style == option,
|
||||||
option.to_string(),
|
option.to_string(),
|
||||||
))
|
))
|
||||||
@@ -494,7 +494,7 @@ impl eframe::App for KomorebiGui {
|
|||||||
StackbarMode::Always,
|
StackbarMode::Always,
|
||||||
] {
|
] {
|
||||||
if ui
|
if ui
|
||||||
.add(egui::SelectableLabel::new(
|
.add(egui::Button::selectable(
|
||||||
self.stackbar_config.mode == option,
|
self.stackbar_config.mode == option,
|
||||||
option.to_string(),
|
option.to_string(),
|
||||||
))
|
))
|
||||||
@@ -513,7 +513,7 @@ impl eframe::App for KomorebiGui {
|
|||||||
ui.collapsing("Label", |ui| {
|
ui.collapsing("Label", |ui| {
|
||||||
for option in [StackbarLabel::Process, StackbarLabel::Title] {
|
for option in [StackbarLabel::Process, StackbarLabel::Title] {
|
||||||
if ui
|
if ui
|
||||||
.add(egui::SelectableLabel::new(
|
.add(egui::Button::selectable(
|
||||||
self.stackbar_config.label == option,
|
self.stackbar_config.label == option,
|
||||||
option.to_string(),
|
option.to_string(),
|
||||||
))
|
))
|
||||||
@@ -772,7 +772,7 @@ impl eframe::App for KomorebiGui {
|
|||||||
DefaultLayout::Grid,
|
DefaultLayout::Grid,
|
||||||
] {
|
] {
|
||||||
if ui
|
if ui
|
||||||
.add(egui::SelectableLabel::new(
|
.add(egui::Button::selectable(
|
||||||
workspace.layout == option,
|
workspace.layout == option,
|
||||||
option.to_string(),
|
option.to_string(),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "komorebi-layouts"
|
||||||
|
version = "0.1.41"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { workspace = true }
|
||||||
|
color-eyre = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
serde_yaml = { workspace = true }
|
||||||
|
strum = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
|
||||||
|
# Optional dependencies
|
||||||
|
schemars = { workspace = true, optional = true }
|
||||||
|
windows = { workspace = true, optional = true }
|
||||||
|
objc2-core-foundation = { version = "0.3", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"CFCGTypes",
|
||||||
|
], optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
schemars = ["dep:schemars"]
|
||||||
|
win32 = ["dep:windows"]
|
||||||
|
darwin = ["dep:objc2-core-foundation"]
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,7 @@
|
|||||||
|
use color_eyre::eyre;
|
||||||
|
use color_eyre::eyre::bail;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
@@ -5,12 +9,6 @@ use std::ops::Deref;
|
|||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use color_eyre::eyre::anyhow;
|
|
||||||
use color_eyre::eyre::bail;
|
|
||||||
use color_eyre::Result;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use super::Rect;
|
use super::Rect;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
@@ -32,7 +30,7 @@ impl DerefMut for CustomLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CustomLayout {
|
impl CustomLayout {
|
||||||
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
|
pub fn from_path<P: AsRef<Path>>(path: P) -> eyre::Result<Self> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let layout: Self = match path.extension() {
|
let layout: Self = match path.extension() {
|
||||||
Some(extension) if extension == "yaml" || extension == "yml" => {
|
Some(extension) if extension == "yaml" || extension == "yml" => {
|
||||||
@@ -41,7 +39,7 @@ impl CustomLayout {
|
|||||||
Some(extension) if extension == "json" => {
|
Some(extension) if extension == "json" => {
|
||||||
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
||||||
}
|
}
|
||||||
_ => return Err(anyhow!("custom layouts must be json or yaml files")),
|
_ => bail!("custom layouts must be json or yaml files"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !layout.is_valid() {
|
if !layout.is_valid() {
|
||||||
@@ -0,0 +1,789 @@
|
|||||||
|
use clap::ValueEnum;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use strum::Display;
|
||||||
|
use strum::EnumString;
|
||||||
|
|
||||||
|
use super::OperationDirection;
|
||||||
|
use super::Rect;
|
||||||
|
use super::Sizing;
|
||||||
|
|
||||||
|
/// Maximum number of ratio values that can be specified for column_ratios and row_ratios
|
||||||
|
pub const MAX_RATIOS: usize = 5;
|
||||||
|
|
||||||
|
/// Minimum allowed ratio value (prevents zero-sized windows)
|
||||||
|
pub const MIN_RATIO: f32 = 0.1;
|
||||||
|
|
||||||
|
/// Maximum allowed ratio value (ensures space for remaining windows)
|
||||||
|
pub const MAX_RATIO: f32 = 0.9;
|
||||||
|
|
||||||
|
/// Default ratio value when none is specified
|
||||||
|
pub const DEFAULT_RATIO: f32 = 0.5;
|
||||||
|
|
||||||
|
/// Default secondary ratio value for UltrawideVerticalStack layout
|
||||||
|
pub const DEFAULT_SECONDARY_RATIO: f32 = 0.25;
|
||||||
|
|
||||||
|
/// Validates and converts a Vec of ratios into a fixed-size array.
|
||||||
|
/// - Clamps values to MIN_RATIO..MAX_RATIO range
|
||||||
|
/// - Truncates when cumulative sum reaches or exceeds 1.0
|
||||||
|
/// - Limits to MAX_RATIOS values
|
||||||
|
#[must_use]
|
||||||
|
pub fn validate_ratios(ratios: &[f32]) -> [Option<f32>; MAX_RATIOS] {
|
||||||
|
let mut arr = [None; MAX_RATIOS];
|
||||||
|
let mut cumulative_sum = 0.0_f32;
|
||||||
|
|
||||||
|
for (i, &val) in ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||||
|
let clamped_val = val.clamp(MIN_RATIO, MAX_RATIO);
|
||||||
|
|
||||||
|
// Only add this ratio if cumulative sum stays below 1.0
|
||||||
|
if cumulative_sum + clamped_val < 1.0 {
|
||||||
|
arr[i] = Some(clamped_val);
|
||||||
|
cumulative_sum += clamped_val;
|
||||||
|
} else {
|
||||||
|
// Stop adding ratios - cumulative sum would reach or exceed 1.0
|
||||||
|
tracing::debug!(
|
||||||
|
"Truncating ratios at index {} - cumulative sum {} + {} would reach/exceed 1.0",
|
||||||
|
i,
|
||||||
|
cumulative_sum,
|
||||||
|
clamped_val
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arr
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
|
||||||
|
)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// A predefined komorebi layout
|
||||||
|
pub enum DefaultLayout {
|
||||||
|
/// BSP Layout
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// +-------+-----+
|
||||||
|
/// | | |
|
||||||
|
/// | +--+--+
|
||||||
|
/// | | |--|
|
||||||
|
/// +-------+--+--+
|
||||||
|
/// ```
|
||||||
|
BSP,
|
||||||
|
/// Columns Layout
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// +--+--+--+--+
|
||||||
|
/// | | | | |
|
||||||
|
/// | | | | |
|
||||||
|
/// | | | | |
|
||||||
|
/// +--+--+--+--+
|
||||||
|
/// ```
|
||||||
|
Columns,
|
||||||
|
/// Rows Layout
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// +-----------+
|
||||||
|
/// |-----------|
|
||||||
|
/// |-----------|
|
||||||
|
/// |-----------|
|
||||||
|
/// +-----------+
|
||||||
|
/// ```
|
||||||
|
Rows,
|
||||||
|
/// Vertical Stack Layout
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// +-------+-----+
|
||||||
|
/// | | |
|
||||||
|
/// | +-----+
|
||||||
|
/// | | |
|
||||||
|
/// +-------+-----+
|
||||||
|
/// ```
|
||||||
|
VerticalStack,
|
||||||
|
/// Horizontal Stack Layout
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// +------+------+
|
||||||
|
/// | |
|
||||||
|
/// |------+------+
|
||||||
|
/// | | |
|
||||||
|
/// +------+------+
|
||||||
|
/// ```
|
||||||
|
HorizontalStack,
|
||||||
|
/// Ultrawide Vertical Stack Layout
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// +-----+-----------+-----+
|
||||||
|
/// | | | |
|
||||||
|
/// | | +-----+
|
||||||
|
/// | | | |
|
||||||
|
/// | | +-----+
|
||||||
|
/// | | | |
|
||||||
|
/// +-----+-----------+-----+
|
||||||
|
/// ```
|
||||||
|
UltrawideVerticalStack,
|
||||||
|
/// Grid Layout
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// +-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||||
|
/// | | | | | | | | | | | | | | |
|
||||||
|
/// | | | | | | | | | | | | | +---+
|
||||||
|
/// +-----+-----+ | +---+---+ +---+---+---+ +---+---| |
|
||||||
|
/// | | | | | | | | | | | | | +---+
|
||||||
|
/// | | | | | | | | | | | | | | |
|
||||||
|
/// +-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||||
|
/// 4 windows 5 windows 6 windows 7 windows
|
||||||
|
/// ```
|
||||||
|
Grid,
|
||||||
|
/// Right Main Vertical Stack Layout
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// +-----+-------+
|
||||||
|
/// | | |
|
||||||
|
/// +-----+ |
|
||||||
|
/// | | |
|
||||||
|
/// +-----+-------+
|
||||||
|
/// ```
|
||||||
|
RightMainVerticalStack,
|
||||||
|
/// Scrolling Layout
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// +--+--+--+--+--+--+
|
||||||
|
/// | | | |
|
||||||
|
/// | | | |
|
||||||
|
/// | | | |
|
||||||
|
/// +--+--+--+--+--+--+
|
||||||
|
/// ```
|
||||||
|
Scrolling,
|
||||||
|
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to deserialize a variable-length array into a fixed [Option<f32>; MAX_RATIOS]
|
||||||
|
/// Ratios are truncated when their cumulative sum reaches or exceeds 1.0 to ensure
|
||||||
|
/// there's always remaining space for additional windows.
|
||||||
|
fn deserialize_ratios<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<Option<[Option<f32>; MAX_RATIOS]>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let opt: Option<Vec<f32>> = Option::deserialize(deserializer)?;
|
||||||
|
Ok(opt.map(|vec| validate_ratios(&vec)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to serialize [Option<f32>; MAX_RATIOS] as a compact array (without trailing nulls)
|
||||||
|
fn serialize_ratios<S>(
|
||||||
|
value: &Option<[Option<f32>; MAX_RATIOS]>,
|
||||||
|
serializer: S,
|
||||||
|
) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
None => serializer.serialize_none(),
|
||||||
|
Some(arr) => {
|
||||||
|
// Find last non-None index
|
||||||
|
let last_idx = arr
|
||||||
|
.iter()
|
||||||
|
.rposition(|x| x.is_some())
|
||||||
|
.map(|i| i + 1)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let vec: Vec<f32> = arr.iter().take(last_idx).filter_map(|&x| x).collect();
|
||||||
|
serializer.serialize_some(&vec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Options for specific layouts
|
||||||
|
pub struct LayoutOptions {
|
||||||
|
/// Options related to the Scrolling layout
|
||||||
|
pub scrolling: Option<ScrollingLayoutOptions>,
|
||||||
|
/// Options related to the Grid layout
|
||||||
|
pub grid: Option<GridLayoutOptions>,
|
||||||
|
/// Column width ratios (up to MAX_RATIOS values between 0.1 and 0.9)
|
||||||
|
///
|
||||||
|
/// - Used by Columns layout: ratios for each column width
|
||||||
|
/// - Used by Grid layout: ratios for column widths
|
||||||
|
/// - Used by BSP, VerticalStack, RightMainVerticalStack: column_ratios[0] as primary split ratio
|
||||||
|
/// - Used by HorizontalStack: column_ratios[0] as primary split ratio (top area height)
|
||||||
|
/// - Used by UltrawideVerticalStack: column_ratios[0] as center ratio, column_ratios[1] as left ratio
|
||||||
|
///
|
||||||
|
/// Columns without a ratio share remaining space equally.
|
||||||
|
/// Example: `[0.3, 0.4, 0.3]` for 30%-40%-30% columns
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
deserialize_with = "deserialize_ratios",
|
||||||
|
serialize_with = "serialize_ratios"
|
||||||
|
)]
|
||||||
|
pub column_ratios: Option<[Option<f32>; MAX_RATIOS]>,
|
||||||
|
/// Row height ratios (up to MAX_RATIOS values between 0.1 and 0.9)
|
||||||
|
///
|
||||||
|
/// - Used by Rows layout: ratios for each row height
|
||||||
|
/// - Used by Grid layout: ratios for row heights
|
||||||
|
///
|
||||||
|
/// Rows without a ratio share remaining space equally.
|
||||||
|
/// Example: `[0.5, 0.5]` for 50%-50% rows
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
deserialize_with = "deserialize_ratios",
|
||||||
|
serialize_with = "serialize_ratios"
|
||||||
|
)]
|
||||||
|
pub row_ratios: Option<[Option<f32>; MAX_RATIOS]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Options for the Scrolling layout
|
||||||
|
pub struct ScrollingLayoutOptions {
|
||||||
|
/// Desired number of visible columns (default: 3)
|
||||||
|
pub columns: usize,
|
||||||
|
/// With an odd number of visible columns, keep the focused window column centered
|
||||||
|
pub center_focused_column: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Options for the Grid layout
|
||||||
|
pub struct GridLayoutOptions {
|
||||||
|
/// Maximum number of rows per grid column
|
||||||
|
pub rows: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultLayout {
|
||||||
|
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::UltrawideVerticalStack | Self::RightMainVerticalStack => match len {
|
||||||
|
n if n > 1 => 1,
|
||||||
|
_ => 0,
|
||||||
|
},
|
||||||
|
Self::Scrolling => 0,
|
||||||
|
DefaultLayout::BSP
|
||||||
|
| DefaultLayout::Columns
|
||||||
|
| DefaultLayout::Rows
|
||||||
|
| DefaultLayout::VerticalStack
|
||||||
|
| DefaultLayout::HorizontalStack
|
||||||
|
| DefaultLayout::Grid => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rightmost_index(&self, len: usize) -> usize {
|
||||||
|
match self {
|
||||||
|
DefaultLayout::BSP
|
||||||
|
| DefaultLayout::Columns
|
||||||
|
| DefaultLayout::Rows
|
||||||
|
| DefaultLayout::VerticalStack
|
||||||
|
| DefaultLayout::HorizontalStack
|
||||||
|
| DefaultLayout::Grid => len.saturating_sub(1),
|
||||||
|
DefaultLayout::UltrawideVerticalStack => match len {
|
||||||
|
2 => 0,
|
||||||
|
_ => len.saturating_sub(1),
|
||||||
|
},
|
||||||
|
DefaultLayout::RightMainVerticalStack => 0,
|
||||||
|
DefaultLayout::Scrolling => len.saturating_sub(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
|
||||||
|
pub fn resize(
|
||||||
|
&self,
|
||||||
|
unaltered: &Rect,
|
||||||
|
resize: &Option<Rect>,
|
||||||
|
edge: OperationDirection,
|
||||||
|
sizing: Sizing,
|
||||||
|
delta: i32,
|
||||||
|
) -> Option<Rect> {
|
||||||
|
if !matches!(
|
||||||
|
self,
|
||||||
|
Self::BSP
|
||||||
|
| Self::Columns
|
||||||
|
| Self::Rows
|
||||||
|
| Self::VerticalStack
|
||||||
|
| Self::RightMainVerticalStack
|
||||||
|
| Self::HorizontalStack
|
||||||
|
| Self::UltrawideVerticalStack
|
||||||
|
| Self::Scrolling
|
||||||
|
) {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut r = resize.unwrap_or_default();
|
||||||
|
|
||||||
|
let resize_delta = delta;
|
||||||
|
|
||||||
|
match edge {
|
||||||
|
OperationDirection::Left => match sizing {
|
||||||
|
Sizing::Increase => {
|
||||||
|
// Some final checks to make sure the user can't infinitely resize to
|
||||||
|
// the point of pushing other windows out of bounds
|
||||||
|
|
||||||
|
// Note: These checks cannot take into account the changes made to the
|
||||||
|
// edges of adjacent windows at operation time, so it is still possible
|
||||||
|
// to push windows out of bounds by maxing out an Increase Left on a
|
||||||
|
// Window with index 1, and then maxing out a Decrease Right on a Window
|
||||||
|
// with index 0. I don't think it's worth trying to defensively program
|
||||||
|
// against this; if people end up in this situation they are better off
|
||||||
|
// just hitting the retile command
|
||||||
|
let diff = ((r.left + -resize_delta) as f32).abs();
|
||||||
|
if diff < unaltered.right as f32 {
|
||||||
|
r.left += -resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sizing::Decrease => {
|
||||||
|
let diff = ((r.left - -resize_delta) as f32).abs();
|
||||||
|
if diff < unaltered.right as f32 {
|
||||||
|
r.left -= -resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OperationDirection::Up => match sizing {
|
||||||
|
Sizing::Increase => {
|
||||||
|
let diff = ((r.top + resize_delta) as f32).abs();
|
||||||
|
if diff < unaltered.bottom as f32 {
|
||||||
|
r.top += -resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sizing::Decrease => {
|
||||||
|
let diff = ((r.top - resize_delta) as f32).abs();
|
||||||
|
if diff < unaltered.bottom as f32 {
|
||||||
|
r.top -= -resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OperationDirection::Right => match sizing {
|
||||||
|
Sizing::Increase => {
|
||||||
|
let diff = ((r.right + resize_delta) as f32).abs();
|
||||||
|
if diff < unaltered.right as f32 {
|
||||||
|
r.right += resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sizing::Decrease => {
|
||||||
|
let diff = ((r.right - resize_delta) as f32).abs();
|
||||||
|
if diff < unaltered.right as f32 {
|
||||||
|
r.right -= resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OperationDirection::Down => match sizing {
|
||||||
|
Sizing::Increase => {
|
||||||
|
let diff = ((r.bottom + resize_delta) as f32).abs();
|
||||||
|
if diff < unaltered.bottom as f32 {
|
||||||
|
r.bottom += resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sizing::Decrease => {
|
||||||
|
let diff = ((r.bottom - resize_delta) as f32).abs();
|
||||||
|
if diff < unaltered.bottom as f32 {
|
||||||
|
r.bottom -= resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if r.eq(&Rect::default()) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Option::from(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn cycle_next(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::BSP => Self::Columns,
|
||||||
|
Self::Columns => Self::Rows,
|
||||||
|
Self::Rows => Self::VerticalStack,
|
||||||
|
Self::VerticalStack => Self::HorizontalStack,
|
||||||
|
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||||
|
Self::UltrawideVerticalStack => Self::Grid,
|
||||||
|
Self::Grid => Self::RightMainVerticalStack,
|
||||||
|
Self::RightMainVerticalStack => Self::Scrolling,
|
||||||
|
Self::Scrolling => Self::BSP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn cycle_previous(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling => Self::RightMainVerticalStack,
|
||||||
|
Self::RightMainVerticalStack => Self::Grid,
|
||||||
|
Self::Grid => Self::UltrawideVerticalStack,
|
||||||
|
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||||
|
Self::HorizontalStack => Self::VerticalStack,
|
||||||
|
Self::VerticalStack => Self::Rows,
|
||||||
|
Self::Rows => Self::Columns,
|
||||||
|
Self::Columns => Self::BSP,
|
||||||
|
Self::BSP => Self::RightMainVerticalStack,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
use super::custom_layout::Column;
|
|
||||||
use super::custom_layout::ColumnSplit;
|
|
||||||
use super::custom_layout::ColumnSplitWithCapacity;
|
|
||||||
use super::custom_layout::CustomLayout;
|
|
||||||
use super::DefaultLayout;
|
use super::DefaultLayout;
|
||||||
use super::OperationDirection;
|
use super::OperationDirection;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
|
use super::custom_layout::Column;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
|
use super::custom_layout::ColumnSplit;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
|
use super::custom_layout::ColumnSplitWithCapacity;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
|
use super::custom_layout::CustomLayout;
|
||||||
|
use crate::default_layout::LayoutOptions;
|
||||||
|
|
||||||
pub trait Direction {
|
pub trait Direction {
|
||||||
fn index_in_direction(
|
fn index_in_direction(
|
||||||
@@ -11,6 +16,7 @@ pub trait Direction {
|
|||||||
op_direction: OperationDirection,
|
op_direction: OperationDirection,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> Option<usize>;
|
) -> Option<usize>;
|
||||||
|
|
||||||
fn is_valid_direction(
|
fn is_valid_direction(
|
||||||
@@ -18,30 +24,35 @@ pub trait Direction {
|
|||||||
op_direction: OperationDirection,
|
op_direction: OperationDirection,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> bool;
|
) -> bool;
|
||||||
fn up_index(
|
fn up_index(
|
||||||
&self,
|
&self,
|
||||||
op_direction: Option<OperationDirection>,
|
op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: Option<usize>,
|
count: Option<usize>,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> usize;
|
) -> usize;
|
||||||
fn down_index(
|
fn down_index(
|
||||||
&self,
|
&self,
|
||||||
op_direction: Option<OperationDirection>,
|
op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: Option<usize>,
|
count: Option<usize>,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> usize;
|
) -> usize;
|
||||||
fn left_index(
|
fn left_index(
|
||||||
&self,
|
&self,
|
||||||
op_direction: Option<OperationDirection>,
|
op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: Option<usize>,
|
count: Option<usize>,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> usize;
|
) -> usize;
|
||||||
fn right_index(
|
fn right_index(
|
||||||
&self,
|
&self,
|
||||||
op_direction: Option<OperationDirection>,
|
op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: Option<usize>,
|
count: Option<usize>,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> usize;
|
) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,32 +62,53 @@ impl Direction for DefaultLayout {
|
|||||||
op_direction: OperationDirection,
|
op_direction: OperationDirection,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
match op_direction {
|
match op_direction {
|
||||||
OperationDirection::Left => {
|
OperationDirection::Left => {
|
||||||
if self.is_valid_direction(op_direction, idx, count) {
|
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||||
Option::from(self.left_index(Some(op_direction), idx, Some(count)))
|
Option::from(self.left_index(
|
||||||
|
Some(op_direction),
|
||||||
|
idx,
|
||||||
|
Some(count),
|
||||||
|
layout_options,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OperationDirection::Right => {
|
OperationDirection::Right => {
|
||||||
if self.is_valid_direction(op_direction, idx, count) {
|
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||||
Option::from(self.right_index(Some(op_direction), idx, Some(count)))
|
Option::from(self.right_index(
|
||||||
|
Some(op_direction),
|
||||||
|
idx,
|
||||||
|
Some(count),
|
||||||
|
layout_options,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OperationDirection::Up => {
|
OperationDirection::Up => {
|
||||||
if self.is_valid_direction(op_direction, idx, count) {
|
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||||
Option::from(self.up_index(Some(op_direction), idx, Some(count)))
|
Option::from(self.up_index(
|
||||||
|
Some(op_direction),
|
||||||
|
idx,
|
||||||
|
Some(count),
|
||||||
|
layout_options,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OperationDirection::Down => {
|
OperationDirection::Down => {
|
||||||
if self.is_valid_direction(op_direction, idx, count) {
|
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||||
Option::from(self.down_index(Some(op_direction), idx, Some(count)))
|
Option::from(self.down_index(
|
||||||
|
Some(op_direction),
|
||||||
|
idx,
|
||||||
|
Some(count),
|
||||||
|
layout_options,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -89,6 +121,7 @@ impl Direction for DefaultLayout {
|
|||||||
op_direction: OperationDirection,
|
op_direction: OperationDirection,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if count < 2 {
|
if count < 2 {
|
||||||
return false;
|
return false;
|
||||||
@@ -101,17 +134,17 @@ impl Direction for DefaultLayout {
|
|||||||
Self::Rows | Self::HorizontalStack => idx != 0,
|
Self::Rows | Self::HorizontalStack => idx != 0,
|
||||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
||||||
Self::UltrawideVerticalStack => idx > 2,
|
Self::UltrawideVerticalStack => idx > 2,
|
||||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||||
Self::Scrolling => false,
|
Self::Scrolling => false,
|
||||||
},
|
},
|
||||||
OperationDirection::Down => match self {
|
OperationDirection::Down => match self {
|
||||||
Self::BSP => idx != count - 1 && idx % 2 != 0,
|
Self::BSP => idx != count - 1 && !idx.is_multiple_of(2),
|
||||||
Self::Columns => false,
|
Self::Columns => false,
|
||||||
Self::Rows => idx != count - 1,
|
Self::Rows => idx != count - 1,
|
||||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
|
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
|
||||||
Self::HorizontalStack => idx == 0,
|
Self::HorizontalStack => idx == 0,
|
||||||
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||||
Self::Scrolling => false,
|
Self::Scrolling => false,
|
||||||
},
|
},
|
||||||
OperationDirection::Left => match self {
|
OperationDirection::Left => match self {
|
||||||
@@ -121,11 +154,11 @@ impl Direction for DefaultLayout {
|
|||||||
Self::Rows => false,
|
Self::Rows => false,
|
||||||
Self::HorizontalStack => idx != 0 && idx != 1,
|
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||||
Self::UltrawideVerticalStack => idx != 1,
|
Self::UltrawideVerticalStack => idx != 1,
|
||||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||||
Self::Scrolling => idx != 0,
|
Self::Scrolling => idx != 0,
|
||||||
},
|
},
|
||||||
OperationDirection::Right => match self {
|
OperationDirection::Right => match self {
|
||||||
Self::BSP => idx % 2 == 0 && idx != count - 1,
|
Self::BSP => idx.is_multiple_of(2) && idx != count - 1,
|
||||||
Self::Columns => idx != count - 1,
|
Self::Columns => idx != count - 1,
|
||||||
Self::Rows => false,
|
Self::Rows => false,
|
||||||
Self::VerticalStack => idx == 0,
|
Self::VerticalStack => idx == 0,
|
||||||
@@ -135,7 +168,7 @@ impl Direction for DefaultLayout {
|
|||||||
2 => idx != 0,
|
2 => idx != 0,
|
||||||
_ => idx < 2,
|
_ => idx < 2,
|
||||||
},
|
},
|
||||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||||
Self::Scrolling => idx != count - 1,
|
Self::Scrolling => idx != count - 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -146,10 +179,11 @@ impl Direction for DefaultLayout {
|
|||||||
op_direction: Option<OperationDirection>,
|
op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: Option<usize>,
|
count: Option<usize>,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::BSP => {
|
Self::BSP => {
|
||||||
if idx % 2 == 0 {
|
if idx.is_multiple_of(2) {
|
||||||
idx - 1
|
idx - 1
|
||||||
} else {
|
} else {
|
||||||
idx - 2
|
idx - 2
|
||||||
@@ -161,7 +195,7 @@ impl Direction for DefaultLayout {
|
|||||||
| Self::UltrawideVerticalStack
|
| Self::UltrawideVerticalStack
|
||||||
| Self::RightMainVerticalStack => idx - 1,
|
| Self::RightMainVerticalStack => idx - 1,
|
||||||
Self::HorizontalStack => 0,
|
Self::HorizontalStack => 0,
|
||||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||||
Self::Scrolling => unreachable!(),
|
Self::Scrolling => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,6 +205,7 @@ impl Direction for DefaultLayout {
|
|||||||
op_direction: Option<OperationDirection>,
|
op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: Option<usize>,
|
count: Option<usize>,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::BSP
|
Self::BSP
|
||||||
@@ -180,7 +215,7 @@ impl Direction for DefaultLayout {
|
|||||||
| Self::RightMainVerticalStack => idx + 1,
|
| Self::RightMainVerticalStack => idx + 1,
|
||||||
Self::Columns => unreachable!(),
|
Self::Columns => unreachable!(),
|
||||||
Self::HorizontalStack => 1,
|
Self::HorizontalStack => 1,
|
||||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||||
Self::Scrolling => unreachable!(),
|
Self::Scrolling => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,10 +225,11 @@ impl Direction for DefaultLayout {
|
|||||||
op_direction: Option<OperationDirection>,
|
op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: Option<usize>,
|
count: Option<usize>,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::BSP => {
|
Self::BSP => {
|
||||||
if idx % 2 == 0 {
|
if idx.is_multiple_of(2) {
|
||||||
idx - 2
|
idx - 2
|
||||||
} else {
|
} else {
|
||||||
idx - 1
|
idx - 1
|
||||||
@@ -208,7 +244,7 @@ impl Direction for DefaultLayout {
|
|||||||
1 => unreachable!(),
|
1 => unreachable!(),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
},
|
},
|
||||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||||
Self::Scrolling => idx - 1,
|
Self::Scrolling => idx - 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,6 +254,7 @@ impl Direction for DefaultLayout {
|
|||||||
op_direction: Option<OperationDirection>,
|
op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: Option<usize>,
|
count: Option<usize>,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
|
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
|
||||||
@@ -229,7 +266,7 @@ impl Direction for DefaultLayout {
|
|||||||
0 => 2,
|
0 => 2,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
},
|
},
|
||||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||||
Self::Scrolling => idx + 1,
|
Self::Scrolling => idx + 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,21 +297,32 @@ struct GridTouchingEdges {
|
|||||||
clippy::cast_precision_loss,
|
clippy::cast_precision_loss,
|
||||||
clippy::cast_sign_loss
|
clippy::cast_sign_loss
|
||||||
)]
|
)]
|
||||||
fn get_grid_item(idx: usize, count: usize) -> GridItem {
|
fn get_grid_item(idx: usize, count: usize, layout_options: Option<LayoutOptions>) -> GridItem {
|
||||||
let num_cols = (count as f32).sqrt().ceil() as usize;
|
let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
|
||||||
|
let num_cols = if let Some(rows) = row_constraint {
|
||||||
|
((count as f32) / (rows as f32)).ceil() as i32
|
||||||
|
} else {
|
||||||
|
(count as f32).sqrt().ceil() as i32
|
||||||
|
};
|
||||||
|
|
||||||
let mut iter = 0;
|
let mut iter = 0;
|
||||||
|
|
||||||
for col in 0..num_cols {
|
for col in 0..num_cols {
|
||||||
let remaining_windows = count - iter;
|
let remaining_windows = (count - iter) as i32;
|
||||||
let remaining_columns = num_cols - col;
|
let remaining_columns = num_cols - col;
|
||||||
let num_rows_in_this_col = remaining_windows / remaining_columns;
|
|
||||||
|
let num_rows_in_this_col = if let Some(rows) = row_constraint {
|
||||||
|
(remaining_windows / remaining_columns).min(rows as i32)
|
||||||
|
} else {
|
||||||
|
remaining_windows / remaining_columns
|
||||||
|
};
|
||||||
|
|
||||||
for row in 0..num_rows_in_this_col {
|
for row in 0..num_rows_in_this_col {
|
||||||
if iter == idx {
|
if iter == idx {
|
||||||
return GridItem {
|
return GridItem {
|
||||||
state: GridItemState::Valid,
|
state: GridItemState::Valid,
|
||||||
row: row + 1,
|
row: (row + 1) as usize,
|
||||||
num_rows: num_rows_in_this_col,
|
num_rows: num_rows_in_this_col as usize,
|
||||||
touching_edges: GridTouchingEdges {
|
touching_edges: GridTouchingEdges {
|
||||||
left: col == 0,
|
left: col == 0,
|
||||||
right: col == num_cols - 1,
|
right: col == num_cols - 1,
|
||||||
@@ -301,8 +349,13 @@ fn get_grid_item(idx: usize, count: usize) -> GridItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_grid_edge(op_direction: OperationDirection, idx: usize, count: usize) -> bool {
|
fn is_grid_edge(
|
||||||
let item = get_grid_item(idx, count);
|
op_direction: OperationDirection,
|
||||||
|
idx: usize,
|
||||||
|
count: usize,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
|
) -> bool {
|
||||||
|
let item = get_grid_item(idx, count, layout_options);
|
||||||
|
|
||||||
match item.state {
|
match item.state {
|
||||||
GridItemState::Invalid => false,
|
GridItemState::Invalid => false,
|
||||||
@@ -319,6 +372,7 @@ fn grid_neighbor(
|
|||||||
op_direction: Option<OperationDirection>,
|
op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: Option<usize>,
|
count: Option<usize>,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let Some(op_direction) = op_direction else {
|
let Some(op_direction) = op_direction else {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -328,11 +382,11 @@ fn grid_neighbor(
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
let item = get_grid_item(idx, count);
|
let item = get_grid_item(idx, count, layout_options);
|
||||||
|
|
||||||
match op_direction {
|
match op_direction {
|
||||||
OperationDirection::Left => {
|
OperationDirection::Left => {
|
||||||
let item_from_prev_col = get_grid_item(idx - item.row, count);
|
let item_from_prev_col = get_grid_item(idx - item.row, count, layout_options);
|
||||||
|
|
||||||
if item.touching_edges.up && item.num_rows != item_from_prev_col.num_rows {
|
if item.touching_edges.up && item.num_rows != item_from_prev_col.num_rows {
|
||||||
return idx - (item.num_rows - 1);
|
return idx - (item.num_rows - 1);
|
||||||
@@ -350,42 +404,49 @@ fn grid_neighbor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
impl Direction for CustomLayout {
|
impl Direction for CustomLayout {
|
||||||
fn index_in_direction(
|
fn index_in_direction(
|
||||||
&self,
|
&self,
|
||||||
op_direction: OperationDirection,
|
op_direction: OperationDirection,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
if count <= self.len() {
|
if count <= self.len() {
|
||||||
return DefaultLayout::Columns.index_in_direction(op_direction, idx, count);
|
return DefaultLayout::Columns.index_in_direction(
|
||||||
|
op_direction,
|
||||||
|
idx,
|
||||||
|
count,
|
||||||
|
layout_options,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
match op_direction {
|
match op_direction {
|
||||||
OperationDirection::Left => {
|
OperationDirection::Left => {
|
||||||
if self.is_valid_direction(op_direction, idx, count) {
|
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||||
Option::from(self.left_index(None, idx, None))
|
Option::from(self.left_index(None, idx, None, layout_options))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OperationDirection::Right => {
|
OperationDirection::Right => {
|
||||||
if self.is_valid_direction(op_direction, idx, count) {
|
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||||
Option::from(self.right_index(None, idx, None))
|
Option::from(self.right_index(None, idx, None, layout_options))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OperationDirection::Up => {
|
OperationDirection::Up => {
|
||||||
if self.is_valid_direction(op_direction, idx, count) {
|
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||||
Option::from(self.up_index(None, idx, None))
|
Option::from(self.up_index(None, idx, None, layout_options))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OperationDirection::Down => {
|
OperationDirection::Down => {
|
||||||
if self.is_valid_direction(op_direction, idx, count) {
|
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||||
Option::from(self.down_index(None, idx, None))
|
Option::from(self.down_index(None, idx, None, layout_options))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -398,9 +459,15 @@ impl Direction for CustomLayout {
|
|||||||
op_direction: OperationDirection,
|
op_direction: OperationDirection,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if count <= self.len() {
|
if count <= self.len() {
|
||||||
return DefaultLayout::Columns.is_valid_direction(op_direction, idx, count);
|
return DefaultLayout::Columns.is_valid_direction(
|
||||||
|
op_direction,
|
||||||
|
idx,
|
||||||
|
count,
|
||||||
|
layout_options,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
match op_direction {
|
match op_direction {
|
||||||
@@ -444,6 +511,7 @@ impl Direction for CustomLayout {
|
|||||||
_op_direction: Option<OperationDirection>,
|
_op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
_count: Option<usize>,
|
_count: Option<usize>,
|
||||||
|
_layout_options: Option<LayoutOptions>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
idx - 1
|
idx - 1
|
||||||
}
|
}
|
||||||
@@ -453,6 +521,7 @@ impl Direction for CustomLayout {
|
|||||||
_op_direction: Option<OperationDirection>,
|
_op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
_count: Option<usize>,
|
_count: Option<usize>,
|
||||||
|
_layout_options: Option<LayoutOptions>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
idx + 1
|
idx + 1
|
||||||
}
|
}
|
||||||
@@ -462,6 +531,7 @@ impl Direction for CustomLayout {
|
|||||||
_op_direction: Option<OperationDirection>,
|
_op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
_count: Option<usize>,
|
_count: Option<usize>,
|
||||||
|
_layout_options: Option<LayoutOptions>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let column_idx = self.column_for_container_idx(idx);
|
let column_idx = self.column_for_container_idx(idx);
|
||||||
if column_idx - 1 == 0 {
|
if column_idx - 1 == 0 {
|
||||||
@@ -476,6 +546,7 @@ impl Direction for CustomLayout {
|
|||||||
_op_direction: Option<OperationDirection>,
|
_op_direction: Option<OperationDirection>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
_count: Option<usize>,
|
_count: Option<usize>,
|
||||||
|
_layout_options: Option<LayoutOptions>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let column_idx = self.column_for_container_idx(idx);
|
let column_idx = self.column_for_container_idx(idx);
|
||||||
self.first_container_idx(column_idx + 1)
|
self.first_container_idx(column_idx + 1)
|
||||||
@@ -2,6 +2,7 @@ use serde::Deserialize;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use super::Arrangement;
|
use super::Arrangement;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use super::CustomLayout;
|
use super::CustomLayout;
|
||||||
use super::DefaultLayout;
|
use super::DefaultLayout;
|
||||||
use super::Direction;
|
use super::Direction;
|
||||||
@@ -10,6 +11,7 @@ use super::Direction;
|
|||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub enum Layout {
|
pub enum Layout {
|
||||||
Default(DefaultLayout),
|
Default(DefaultLayout),
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
Custom(CustomLayout),
|
Custom(CustomLayout),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ impl Layout {
|
|||||||
pub fn as_boxed_direction(&self) -> Box<dyn Direction> {
|
pub fn as_boxed_direction(&self) -> Box<dyn Direction> {
|
||||||
match self {
|
match self {
|
||||||
Layout::Default(layout) => Box::new(*layout),
|
Layout::Default(layout) => Box::new(*layout),
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
Layout::Custom(layout) => Box::new(layout.clone()),
|
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +29,7 @@ impl Layout {
|
|||||||
pub fn as_boxed_arrangement(&self) -> Box<dyn Arrangement> {
|
pub fn as_boxed_arrangement(&self) -> Box<dyn Arrangement> {
|
||||||
match self {
|
match self {
|
||||||
Layout::Default(layout) => Box::new(*layout),
|
Layout::Default(layout) => Box::new(*layout),
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
Layout::Custom(layout) => Box::new(layout.clone()),
|
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#![warn(clippy::all)]
|
||||||
|
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||||
|
|
||||||
|
//! Layout system for the komorebi window manager.
|
||||||
|
//!
|
||||||
|
//! This crate provides the core layout algorithms and types for arranging windows
|
||||||
|
//! in various configurations. It includes optional Windows-specific functionality
|
||||||
|
//! behind the `win32` feature flag.
|
||||||
|
|
||||||
|
pub mod arrangement;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
|
pub mod custom_layout;
|
||||||
|
pub mod cycle_direction;
|
||||||
|
pub mod default_layout;
|
||||||
|
pub mod direction;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod operation_direction;
|
||||||
|
pub mod rect;
|
||||||
|
pub mod sizing;
|
||||||
|
|
||||||
|
pub use arrangement::*;
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
|
pub use custom_layout::*;
|
||||||
|
pub use cycle_direction::*;
|
||||||
|
pub use default_layout::*;
|
||||||
|
pub use direction::*;
|
||||||
|
pub use layout::*;
|
||||||
|
pub use operation_direction::*;
|
||||||
|
pub use rect::*;
|
||||||
|
pub use sizing::*;
|
||||||
+5
-4
@@ -1,14 +1,14 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
use super::Axis;
|
||||||
|
use super::direction::Direction;
|
||||||
|
use crate::default_layout::LayoutOptions;
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
|
|
||||||
use super::direction::Direction;
|
|
||||||
use super::Axis;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub enum OperationDirection {
|
pub enum OperationDirection {
|
||||||
@@ -57,7 +57,8 @@ impl OperationDirection {
|
|||||||
layout_flip: Option<Axis>,
|
layout_flip: Option<Axis>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
len: NonZeroUsize,
|
len: NonZeroUsize,
|
||||||
|
layout_options: Option<LayoutOptions>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
layout.index_in_direction(self.flip(layout_flip), idx, len.get())
|
layout.index_in_direction(self.flip(layout_flip), idx, len.get(), layout_options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,33 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
use windows::Win32::Foundation::RECT;
|
use windows::Win32::Foundation::RECT;
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
use objc2_core_foundation::CGFloat;
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
use objc2_core_foundation::CGPoint;
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
use objc2_core_foundation::CGRect;
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
use objc2_core_foundation::CGSize;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Rectangle dimensions
|
||||||
pub struct Rect {
|
pub struct Rect {
|
||||||
/// The left point in a Win32 Rect
|
/// Left point of the rectangle
|
||||||
pub left: i32,
|
pub left: i32,
|
||||||
/// The top point in a Win32 Rect
|
/// Top point of the rectangle
|
||||||
pub top: i32,
|
pub top: i32,
|
||||||
/// The right point in a Win32 Rect
|
/// Width of the recentangle (from the left point)
|
||||||
pub right: i32,
|
pub right: i32,
|
||||||
/// The bottom point in a Win32 Rect
|
/// Height of the rectangle (from the top point)
|
||||||
pub bottom: i32,
|
pub bottom: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
impl From<RECT> for Rect {
|
impl From<RECT> for Rect {
|
||||||
fn from(rect: RECT) -> Self {
|
fn from(rect: RECT) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -26,6 +39,7 @@ impl From<RECT> for Rect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
impl From<Rect> for RECT {
|
impl From<Rect> for RECT {
|
||||||
fn from(rect: Rect) -> Self {
|
fn from(rect: Rect) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -37,6 +51,53 @@ impl From<Rect> for RECT {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
impl From<CGSize> for Rect {
|
||||||
|
fn from(value: CGSize) -> Self {
|
||||||
|
Self {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: value.width as i32,
|
||||||
|
bottom: value.height as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
impl From<CGRect> for Rect {
|
||||||
|
fn from(value: CGRect) -> Self {
|
||||||
|
Self {
|
||||||
|
left: value.origin.x as i32,
|
||||||
|
top: value.origin.y as i32,
|
||||||
|
right: value.size.width as i32,
|
||||||
|
bottom: value.size.height as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
impl From<&Rect> for CGRect {
|
||||||
|
fn from(value: &Rect) -> Self {
|
||||||
|
Self {
|
||||||
|
origin: CGPoint {
|
||||||
|
x: value.left as CGFloat,
|
||||||
|
y: value.top as CGFloat,
|
||||||
|
},
|
||||||
|
size: CGSize {
|
||||||
|
width: value.right as CGFloat,
|
||||||
|
height: value.bottom as CGFloat,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
impl From<Rect> for CGRect {
|
||||||
|
fn from(value: Rect) -> Self {
|
||||||
|
CGRect::from(&value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Rect {
|
impl Rect {
|
||||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||||
self.right == rhs.right && self.bottom == rhs.bottom
|
self.right == rhs.right && self.bottom == rhs.bottom
|
||||||
@@ -95,6 +156,7 @@ impl Rect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "win32")]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn rect(&self) -> RECT {
|
pub const fn rect(&self) -> RECT {
|
||||||
RECT {
|
RECT {
|
||||||
@@ -104,4 +166,19 @@ impl Rect {
|
|||||||
bottom: self.top + self.bottom,
|
bottom: self.top + self.bottom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "darwin")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn percentage_within_horizontal_bounds(&self, other: &Rect) -> f64 {
|
||||||
|
let overlap_left = self.left.max(other.left);
|
||||||
|
let overlap_right = (self.left + self.right).min(other.left + other.right);
|
||||||
|
|
||||||
|
let overlap_width = overlap_right - overlap_left;
|
||||||
|
|
||||||
|
if overlap_width <= 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(overlap_width as f64) / (other.right as f64) * 100.0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
use clap::ValueEnum;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use strum::Display;
|
||||||
|
use strum::EnumString;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Sizing
|
||||||
|
pub enum Sizing {
|
||||||
|
/// Increase
|
||||||
|
Increase,
|
||||||
|
/// Decrease
|
||||||
|
Decrease,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sizing {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
|
||||||
|
match self {
|
||||||
|
Self::Increase => value + adjustment,
|
||||||
|
Self::Decrease => {
|
||||||
|
if value > 0 && value - adjustment >= 0 {
|
||||||
|
value - adjustment
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.9" }
|
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.10" }
|
||||||
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.9" }
|
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.10" }
|
||||||
|
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-themes"
|
name = "komorebi-themes"
|
||||||
version = "0.1.38"
|
version = "0.1.41"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "96f26c88d83781f234d42222293ec73d23a39ad8" }
|
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "3f157904c641f0dc80f043449fe0214fc4182425" }
|
||||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "bdaff30959512c4f7ee7304117076a48633d777f", default-features = false, features = ["egui31"] }
|
#catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
|
||||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
|
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a", default-features = false, features = [
|
||||||
|
"egui33",
|
||||||
|
] }
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
schemars = { workspace = true, optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
use hex_color::HexColor;
|
use hex_color::HexColor;
|
||||||
#[cfg(feature = "schemars")]
|
#[cfg(feature = "schemars")]
|
||||||
use schemars::gen::SchemaGenerator;
|
use schemars::Schema;
|
||||||
#[cfg(feature = "schemars")]
|
#[cfg(feature = "schemars")]
|
||||||
use schemars::schema::InstanceType;
|
use schemars::SchemaGenerator;
|
||||||
#[cfg(feature = "schemars")]
|
|
||||||
use schemars::schema::Schema;
|
|
||||||
#[cfg(feature = "schemars")]
|
|
||||||
use schemars::schema::SchemaObject;
|
|
||||||
|
|
||||||
use crate::Color32;
|
use crate::Color32;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -15,6 +11,7 @@ use serde::Serialize;
|
|||||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
/// Colour representation
|
||||||
pub enum Colour {
|
pub enum Colour {
|
||||||
/// Colour represented as RGB
|
/// Colour represented as RGB
|
||||||
Rgb(Rgb),
|
Rgb(Rgb),
|
||||||
@@ -56,22 +53,22 @@ impl From<Colour> for Color32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Colour represented as a Hex string
|
||||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct Hex(pub HexColor);
|
pub struct Hex(pub HexColor);
|
||||||
|
|
||||||
#[cfg(feature = "schemars")]
|
#[cfg(feature = "schemars")]
|
||||||
impl schemars::JsonSchema for Hex {
|
impl schemars::JsonSchema for Hex {
|
||||||
fn schema_name() -> String {
|
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||||
String::from("Hex")
|
std::borrow::Cow::Borrowed("Hex")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||||
SchemaObject {
|
schemars::json_schema!({
|
||||||
instance_type: Some(InstanceType::String.into()),
|
"type": "string",
|
||||||
format: Some("color-hex".to_string()),
|
"format": "color-hex",
|
||||||
..Default::default()
|
"description": "Colour represented as a Hex string"
|
||||||
}
|
})
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +83,7 @@ impl From<Colour> for u32 {
|
|||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Colour represented as RGB
|
||||||
pub struct Rgb {
|
pub struct Rgb {
|
||||||
/// Red
|
/// Red
|
||||||
pub r: u32,
|
pub r: u32,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use crate::Base16ColourPalette;
|
||||||
use crate::colour::Colour;
|
use crate::colour::Colour;
|
||||||
use crate::colour::Hex;
|
use crate::colour::Hex;
|
||||||
use crate::Base16ColourPalette;
|
|
||||||
use hex_color::HexColor;
|
use hex_color::HexColor;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
@@ -12,9 +12,12 @@ use serde::Serialize;
|
|||||||
|
|
||||||
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Theme variant
|
||||||
pub enum ThemeVariant {
|
pub enum ThemeVariant {
|
||||||
#[default]
|
#[default]
|
||||||
|
/// Dark variant
|
||||||
Dark,
|
Dark,
|
||||||
|
/// Light variant
|
||||||
Light,
|
Light,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+349
-7
@@ -4,8 +4,8 @@
|
|||||||
pub mod colour;
|
pub mod colour;
|
||||||
mod generator;
|
mod generator;
|
||||||
|
|
||||||
pub use generator::generate_base16_palette;
|
|
||||||
pub use generator::ThemeVariant;
|
pub use generator::ThemeVariant;
|
||||||
|
pub use generator::generate_base16_palette;
|
||||||
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -16,30 +16,31 @@ use strum::IntoEnumIterator;
|
|||||||
use crate::colour::Colour;
|
use crate::colour::Colour;
|
||||||
pub use base16_egui_themes::Base16;
|
pub use base16_egui_themes::Base16;
|
||||||
pub use catppuccin_egui;
|
pub use catppuccin_egui;
|
||||||
use eframe::egui::style::Selection;
|
|
||||||
use eframe::egui::style::WidgetVisuals;
|
|
||||||
use eframe::egui::style::Widgets;
|
|
||||||
pub use eframe::egui::Color32;
|
pub use eframe::egui::Color32;
|
||||||
use eframe::egui::Shadow;
|
use eframe::egui::Shadow;
|
||||||
use eframe::egui::Stroke;
|
use eframe::egui::Stroke;
|
||||||
use eframe::egui::Style;
|
use eframe::egui::Style;
|
||||||
use eframe::egui::Visuals;
|
use eframe::egui::Visuals;
|
||||||
|
use eframe::egui::style::Selection;
|
||||||
|
use eframe::egui::style::WidgetVisuals;
|
||||||
|
use eframe::egui::style::Widgets;
|
||||||
use serde_variant::to_variant_name;
|
use serde_variant::to_variant_name;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
|
/// Theme
|
||||||
pub enum Theme {
|
pub enum Theme {
|
||||||
/// A theme from catppuccin-egui
|
/// Theme from catppuccin-egui
|
||||||
Catppuccin {
|
Catppuccin {
|
||||||
name: Catppuccin,
|
name: Catppuccin,
|
||||||
accent: Option<CatppuccinValue>,
|
accent: Option<CatppuccinValue>,
|
||||||
},
|
},
|
||||||
/// A theme from base16-egui-themes
|
/// Theme from base16-egui-themes
|
||||||
Base16 {
|
Base16 {
|
||||||
name: Base16,
|
name: Base16,
|
||||||
accent: Option<Base16Value>,
|
accent: Option<Base16Value>,
|
||||||
},
|
},
|
||||||
/// A custom base16 palette
|
/// Custom base16 palette
|
||||||
Custom {
|
Custom {
|
||||||
palette: Box<Base16ColourPalette>,
|
palette: Box<Base16ColourPalette>,
|
||||||
accent: Option<Base16Value>,
|
accent: Option<Base16Value>,
|
||||||
@@ -47,22 +48,39 @@ pub enum Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||||
|
/// Base16 colour palette: https://github.com/chriskempson/base16
|
||||||
pub struct Base16ColourPalette {
|
pub struct Base16ColourPalette {
|
||||||
|
/// Base00
|
||||||
pub base_00: Colour,
|
pub base_00: Colour,
|
||||||
|
/// Base01
|
||||||
pub base_01: Colour,
|
pub base_01: Colour,
|
||||||
|
/// Base02
|
||||||
pub base_02: Colour,
|
pub base_02: Colour,
|
||||||
|
/// Base03
|
||||||
pub base_03: Colour,
|
pub base_03: Colour,
|
||||||
|
/// Base04
|
||||||
pub base_04: Colour,
|
pub base_04: Colour,
|
||||||
|
/// Base05
|
||||||
pub base_05: Colour,
|
pub base_05: Colour,
|
||||||
|
/// Base06
|
||||||
pub base_06: Colour,
|
pub base_06: Colour,
|
||||||
|
/// Base07
|
||||||
pub base_07: Colour,
|
pub base_07: Colour,
|
||||||
|
/// Base08
|
||||||
pub base_08: Colour,
|
pub base_08: Colour,
|
||||||
|
/// Base09
|
||||||
pub base_09: Colour,
|
pub base_09: Colour,
|
||||||
|
/// Base0A
|
||||||
pub base_0a: Colour,
|
pub base_0a: Colour,
|
||||||
|
/// Base0B
|
||||||
pub base_0b: Colour,
|
pub base_0b: Colour,
|
||||||
|
/// Base0C
|
||||||
pub base_0c: Colour,
|
pub base_0c: Colour,
|
||||||
|
/// Base0D
|
||||||
pub base_0d: Colour,
|
pub base_0d: Colour,
|
||||||
|
/// Base0E
|
||||||
pub base_0e: Colour,
|
pub base_0e: Colour,
|
||||||
|
/// Base0F
|
||||||
pub base_0f: Colour,
|
pub base_0f: Colour,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,28 +217,48 @@ impl Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||||
|
/// Base16 value
|
||||||
pub enum Base16Value {
|
pub enum Base16Value {
|
||||||
|
/// Base00
|
||||||
Base00,
|
Base00,
|
||||||
|
/// Base01
|
||||||
Base01,
|
Base01,
|
||||||
|
/// Base02
|
||||||
Base02,
|
Base02,
|
||||||
|
/// Base03
|
||||||
Base03,
|
Base03,
|
||||||
|
/// Base04
|
||||||
Base04,
|
Base04,
|
||||||
|
/// Base05
|
||||||
Base05,
|
Base05,
|
||||||
|
/// Base06
|
||||||
#[default]
|
#[default]
|
||||||
Base06,
|
Base06,
|
||||||
|
/// Base07
|
||||||
Base07,
|
Base07,
|
||||||
|
/// Base08
|
||||||
Base08,
|
Base08,
|
||||||
|
/// Base09
|
||||||
Base09,
|
Base09,
|
||||||
|
/// Base0A
|
||||||
Base0A,
|
Base0A,
|
||||||
|
/// Base0B
|
||||||
Base0B,
|
Base0B,
|
||||||
|
/// Base0C
|
||||||
Base0C,
|
Base0C,
|
||||||
|
/// Base0D
|
||||||
Base0D,
|
Base0D,
|
||||||
|
/// Base0E
|
||||||
Base0E,
|
Base0E,
|
||||||
|
/// Base0F
|
||||||
Base0F,
|
Base0F,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper around a Base16 colour palette
|
||||||
pub enum Base16Wrapper {
|
pub enum Base16Wrapper {
|
||||||
|
/// Predefined Base16 colour palette
|
||||||
Base16(Base16),
|
Base16(Base16),
|
||||||
|
/// Custom Base16 colour palette
|
||||||
Custom(Box<Base16ColourPalette>),
|
Custom(Box<Base16ColourPalette>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,10 +306,15 @@ impl Base16Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||||
|
/// Catppuccin palette
|
||||||
pub enum Catppuccin {
|
pub enum Catppuccin {
|
||||||
|
/// Frappe (https://catppuccin.com/palette#flavor-frappe)
|
||||||
Frappe,
|
Frappe,
|
||||||
|
/// Latte (https://catppuccin.com/palette#flavor-latte)
|
||||||
Latte,
|
Latte,
|
||||||
|
/// Macchiato (https://catppuccin.com/palette#flavor-macchiato)
|
||||||
Macchiato,
|
Macchiato,
|
||||||
|
/// Mocha (https://catppuccin.com/palette#flavor-mocha)
|
||||||
Mocha,
|
Mocha,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,33 +336,60 @@ impl From<Catppuccin> for catppuccin_egui::Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||||
|
/// Catppuccin Value
|
||||||
pub enum CatppuccinValue {
|
pub enum CatppuccinValue {
|
||||||
|
/// Rosewater
|
||||||
Rosewater,
|
Rosewater,
|
||||||
|
/// Flamingo
|
||||||
Flamingo,
|
Flamingo,
|
||||||
|
/// Pink
|
||||||
Pink,
|
Pink,
|
||||||
|
/// Mauve
|
||||||
Mauve,
|
Mauve,
|
||||||
|
/// Red
|
||||||
Red,
|
Red,
|
||||||
|
/// Maroon
|
||||||
Maroon,
|
Maroon,
|
||||||
|
/// Peach
|
||||||
Peach,
|
Peach,
|
||||||
|
/// Yellow
|
||||||
Yellow,
|
Yellow,
|
||||||
|
/// Green
|
||||||
Green,
|
Green,
|
||||||
|
/// Teal
|
||||||
Teal,
|
Teal,
|
||||||
|
/// Sky
|
||||||
Sky,
|
Sky,
|
||||||
|
/// Sapphire
|
||||||
Sapphire,
|
Sapphire,
|
||||||
|
/// Blue
|
||||||
Blue,
|
Blue,
|
||||||
|
/// Lavender
|
||||||
Lavender,
|
Lavender,
|
||||||
#[default]
|
#[default]
|
||||||
|
/// Text
|
||||||
Text,
|
Text,
|
||||||
|
/// Subtext1
|
||||||
Subtext1,
|
Subtext1,
|
||||||
|
/// Subtext0
|
||||||
Subtext0,
|
Subtext0,
|
||||||
|
/// Overlay2
|
||||||
Overlay2,
|
Overlay2,
|
||||||
|
/// Overlay1
|
||||||
Overlay1,
|
Overlay1,
|
||||||
|
/// Overlay0
|
||||||
Overlay0,
|
Overlay0,
|
||||||
|
/// Surface2
|
||||||
Surface2,
|
Surface2,
|
||||||
|
/// Surface1
|
||||||
Surface1,
|
Surface1,
|
||||||
|
/// Surface0
|
||||||
Surface0,
|
Surface0,
|
||||||
|
/// Base
|
||||||
Base,
|
Base,
|
||||||
|
/// Mantle
|
||||||
Mantle,
|
Mantle,
|
||||||
|
/// Crust
|
||||||
Crust,
|
Crust,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,3 +429,275 @@ impl CatppuccinValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||||
|
/// Theme from catppuccin-egui
|
||||||
|
pub struct KomorebiThemeCatppuccin {
|
||||||
|
/// Name of the Catppuccin theme (previews: https://github.com/catppuccin/catppuccin)
|
||||||
|
pub name: Catppuccin,
|
||||||
|
/// Single window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||||
|
pub single_border: Option<CatppuccinValue>,
|
||||||
|
/// Stack window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Green)))]
|
||||||
|
pub stack_border: Option<CatppuccinValue>,
|
||||||
|
/// Monocle window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Pink)))]
|
||||||
|
pub monocle_border: Option<CatppuccinValue>,
|
||||||
|
/// Floating window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Yellow)))]
|
||||||
|
pub floating_border: Option<CatppuccinValue>,
|
||||||
|
/// Unfocused window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Base)))]
|
||||||
|
pub unfocused_border: Option<CatppuccinValue>,
|
||||||
|
/// Unfocused locked window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Red)))]
|
||||||
|
pub unfocused_locked_border: Option<CatppuccinValue>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
/// Stackbar focused text colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Green)))]
|
||||||
|
pub stackbar_focused_text: Option<CatppuccinValue>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
/// Stackbar unfocused text colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Text)))]
|
||||||
|
pub stackbar_unfocused_text: Option<CatppuccinValue>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
/// Stackbar background colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Base)))]
|
||||||
|
pub stackbar_background: Option<CatppuccinValue>,
|
||||||
|
/// Bar accent colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||||
|
pub bar_accent: Option<CatppuccinValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||||
|
/// Theme from base16-egui-themes
|
||||||
|
pub struct KomorebiThemeBase16 {
|
||||||
|
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||||
|
pub name: Base16,
|
||||||
|
/// Single window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||||
|
pub single_border: Option<Base16Value>,
|
||||||
|
/// Stack window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||||
|
pub stack_border: Option<Base16Value>,
|
||||||
|
/// Monocle window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0F)))]
|
||||||
|
pub monocle_border: Option<Base16Value>,
|
||||||
|
/// Floating window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base09)))]
|
||||||
|
pub floating_border: Option<Base16Value>,
|
||||||
|
/// Unfocused window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||||
|
pub unfocused_border: Option<Base16Value>,
|
||||||
|
/// Unfocused locked window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base08)))]
|
||||||
|
pub unfocused_locked_border: Option<Base16Value>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
/// Stackbar focused text colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||||
|
pub stackbar_focused_text: Option<Base16Value>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
/// Stackbar unfocused text colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base05)))]
|
||||||
|
pub stackbar_unfocused_text: Option<Base16Value>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
/// Stackbar background colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||||
|
pub stackbar_background: Option<Base16Value>,
|
||||||
|
/// Bar accent colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||||
|
pub bar_accent: Option<Base16Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||||
|
/// Custom Base16 theme
|
||||||
|
pub struct KomorebiThemeCustom {
|
||||||
|
/// Colours of the custom Base16 theme palette
|
||||||
|
pub colours: Box<Base16ColourPalette>,
|
||||||
|
/// Single window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||||
|
pub single_border: Option<Base16Value>,
|
||||||
|
/// Stack window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||||
|
pub stack_border: Option<Base16Value>,
|
||||||
|
/// Monocle window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0F)))]
|
||||||
|
pub monocle_border: Option<Base16Value>,
|
||||||
|
/// Floating window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base09)))]
|
||||||
|
pub floating_border: Option<Base16Value>,
|
||||||
|
/// Unfocused window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||||
|
pub unfocused_border: Option<Base16Value>,
|
||||||
|
/// Unfocused locked window border colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base08)))]
|
||||||
|
pub unfocused_locked_border: Option<Base16Value>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
/// Stackbar focused text colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||||
|
pub stackbar_focused_text: Option<Base16Value>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
/// Stackbar unfocused text colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base05)))]
|
||||||
|
pub stackbar_unfocused_text: Option<Base16Value>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
/// Stackbar background colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||||
|
pub stackbar_background: Option<Base16Value>,
|
||||||
|
/// Bar accent colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||||
|
pub bar_accent: Option<Base16Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||||
|
#[serde(tag = "palette")]
|
||||||
|
/// Komorebi theme
|
||||||
|
pub enum KomorebiTheme {
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Catppuccin"))]
|
||||||
|
/// Theme from catppuccin-egui
|
||||||
|
Catppuccin(KomorebiThemeCatppuccin),
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Base16"))]
|
||||||
|
/// Theme from base16-egui-themes
|
||||||
|
Base16(KomorebiThemeBase16),
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||||
|
/// Custom Base16 theme
|
||||||
|
Custom(KomorebiThemeCustom),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||||
|
/// Theme from catppuccin-egui
|
||||||
|
pub struct KomobarThemeCatppuccin {
|
||||||
|
/// Name of the Catppuccin theme (previews: https://github.com/catppuccin/catppuccin)
|
||||||
|
pub name: Catppuccin,
|
||||||
|
/// Accent colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||||
|
pub accent: Option<CatppuccinValue>,
|
||||||
|
/// Auto select fill colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auto_select_fill: Option<CatppuccinValue>,
|
||||||
|
/// Auto select text colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auto_select_text: Option<CatppuccinValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||||
|
/// Theme from base16-egui-themes
|
||||||
|
pub struct KomobarThemeBase16 {
|
||||||
|
/// Name of the Base16 theme (previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||||
|
pub name: Base16,
|
||||||
|
/// Accent colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||||
|
pub accent: Option<Base16Value>,
|
||||||
|
/// Auto select fill colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auto_select_fill: Option<Base16Value>,
|
||||||
|
/// Auto select text colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auto_select_text: Option<Base16Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||||
|
/// Theme from base16-egui-themes
|
||||||
|
pub struct KomobarThemeCustom {
|
||||||
|
/// Colours of the custom Base16 theme palette
|
||||||
|
pub colours: Box<Base16ColourPalette>,
|
||||||
|
/// Accent colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||||
|
pub accent: Option<Base16Value>,
|
||||||
|
/// Auto select fill colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auto_select_fill: Option<Base16Value>,
|
||||||
|
/// Auto select text colour
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auto_select_text: Option<Base16Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||||
|
#[serde(tag = "palette")]
|
||||||
|
/// Komorebi bar theme
|
||||||
|
pub enum KomobarTheme {
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Catppuccin"))]
|
||||||
|
/// Theme from catppuccin-egui
|
||||||
|
Catppuccin(KomobarThemeCatppuccin),
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Base16"))]
|
||||||
|
/// Theme from base16-egui-themes
|
||||||
|
Base16(KomobarThemeBase16),
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||||
|
/// Custom Base16 theme
|
||||||
|
Custom(KomobarThemeCustom),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<KomorebiTheme> for KomobarTheme {
|
||||||
|
fn from(value: KomorebiTheme) -> Self {
|
||||||
|
match value {
|
||||||
|
KomorebiTheme::Catppuccin(KomorebiThemeCatppuccin {
|
||||||
|
name, bar_accent, ..
|
||||||
|
}) => Self::Catppuccin(KomobarThemeCatppuccin {
|
||||||
|
name,
|
||||||
|
accent: bar_accent,
|
||||||
|
auto_select_fill: None,
|
||||||
|
auto_select_text: None,
|
||||||
|
}),
|
||||||
|
KomorebiTheme::Base16(KomorebiThemeBase16 {
|
||||||
|
name, bar_accent, ..
|
||||||
|
}) => Self::Base16(KomobarThemeBase16 {
|
||||||
|
name,
|
||||||
|
accent: bar_accent,
|
||||||
|
auto_select_fill: None,
|
||||||
|
auto_select_text: None,
|
||||||
|
}),
|
||||||
|
KomorebiTheme::Custom(KomorebiThemeCustom {
|
||||||
|
colours,
|
||||||
|
bar_accent,
|
||||||
|
..
|
||||||
|
}) => Self::Custom(KomobarThemeCustom {
|
||||||
|
colours,
|
||||||
|
accent: bar_accent,
|
||||||
|
auto_select_fill: None,
|
||||||
|
auto_select_text: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+11
-7
@@ -1,23 +1,26 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi"
|
name = "komorebi"
|
||||||
version = "0.1.38"
|
version = "0.1.41"
|
||||||
description = "A tiling window manager for Windows"
|
description = "A tiling window manager for Windows"
|
||||||
repository = "https://github.com/LGUG2Z/komorebi"
|
repository = "https://github.com/LGUG2Z/komorebi"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
komorebi-layouts = { path = "../komorebi-layouts", features = ["win32"] }
|
||||||
komorebi-themes = { path = "../komorebi-themes" }
|
komorebi-themes = { path = "../komorebi-themes" }
|
||||||
|
|
||||||
|
base64 = "0.22"
|
||||||
bitflags = { version = "2", features = ["serde"] }
|
bitflags = { version = "2", features = ["serde"] }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
color-eyre = { workspace = true }
|
color-eyre = { workspace = true }
|
||||||
crossbeam-channel = { workspace = true }
|
crossbeam-channel = { workspace = true }
|
||||||
crossbeam-utils = { workspace = true }
|
crossbeam-utils = { workspace = true }
|
||||||
ctrlc = { version = "3", features = ["termination"] }
|
ctrlc = { version = "3", features = ["termination"] }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
getset = "0.1"
|
ed25519-dalek = "2"
|
||||||
hotwatch = { workspace = true }
|
hotwatch = { workspace = true }
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
miow = "0.6"
|
miow = "0.6"
|
||||||
@@ -28,9 +31,10 @@ parking_lot = { workspace = true }
|
|||||||
paste = { workspace = true }
|
paste = { workspace = true }
|
||||||
powershell_script = "1.0"
|
powershell_script = "1.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
reqwest = { version = "0.12", features = ["blocking"] }
|
||||||
schemars = { workspace = true, optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||||
serde_yaml = { workspace = true }
|
serde_yaml = { workspace = true }
|
||||||
shadow-rs = { workspace = true }
|
shadow-rs = { workspace = true }
|
||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
@@ -47,8 +51,8 @@ windows-numerics = { workspace = true }
|
|||||||
windows-implement = { workspace = true }
|
windows-implement = { workspace = true }
|
||||||
windows-interface = { workspace = true }
|
windows-interface = { workspace = true }
|
||||||
winput = "0.2"
|
winput = "0.2"
|
||||||
winreg = "0.55"
|
winreg = "0.56"
|
||||||
serde_with = { version = "3.12", features = ["schemars_0_8"] }
|
serde_with = { version = "3.12", features = ["schemars_1"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
shadow-rs = { workspace = true }
|
shadow-rs = { workspace = true }
|
||||||
@@ -60,4 +64,4 @@ uuid = { version = "1", features = ["v4"] }
|
|||||||
[features]
|
[features]
|
||||||
default = ["schemars"]
|
default = ["schemars"]
|
||||||
deadlock_detection = ["parking_lot/deadlock_detection"]
|
deadlock_detection = ["parking_lot/deadlock_detection"]
|
||||||
schemars = ["dep:schemars"]
|
schemars = ["dep:schemars", "komorebi-layouts/schemars"]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::hash_map::Entry;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
|
||||||
use super::prefix::AnimationPrefix;
|
use super::prefix::AnimationPrefix;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use color_eyre::Result;
|
use color_eyre::eyre;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -6,10 +6,10 @@ use std::sync::atomic::Ordering;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use super::RenderDispatcher;
|
|
||||||
use super::ANIMATION_DURATION_GLOBAL;
|
use super::ANIMATION_DURATION_GLOBAL;
|
||||||
use super::ANIMATION_FPS;
|
use super::ANIMATION_FPS;
|
||||||
use super::ANIMATION_MANAGER;
|
use super::ANIMATION_MANAGER;
|
||||||
|
use super::RenderDispatcher;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
@@ -55,9 +55,9 @@ impl AnimationEngine {
|
|||||||
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
pub fn animate(
|
pub fn animate(
|
||||||
render_dispatcher: (impl RenderDispatcher + Send + 'static),
|
render_dispatcher: impl RenderDispatcher + Send + 'static,
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
) -> Result<()> {
|
) -> eyre::Result<()> {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let animation_key = render_dispatcher.get_animation_key();
|
let animation_key = render_dispatcher.get_animation_key();
|
||||||
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
|
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::core::Rect;
|
|
||||||
use crate::AnimationStyle;
|
use crate::AnimationStyle;
|
||||||
|
use crate::core::Rect;
|
||||||
|
|
||||||
use super::style::apply_ease_func;
|
use super::style::apply_ease_func;
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ use crate::core::animation::AnimationStyle;
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use prefix::AnimationPrefix;
|
use prefix::AnimationPrefix;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
@@ -25,8 +25,33 @@ use serde::Serialize;
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
/// Animation configuration
|
||||||
|
///
|
||||||
|
/// This can be either global:
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "enabled": true,
|
||||||
|
/// "style": "EaseInSine",
|
||||||
|
/// "fps": 60,
|
||||||
|
/// "duration": 250
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Or scoped by an animation kind prefix:
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "movement": {
|
||||||
|
/// "enabled": true,
|
||||||
|
/// "style": "EaseInSine",
|
||||||
|
/// "fps": 60,
|
||||||
|
/// "duration": 250
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
pub enum PerAnimationPrefixConfig<T> {
|
pub enum PerAnimationPrefixConfig<T> {
|
||||||
|
/// Animation configuration prefixed for a specific animation kind
|
||||||
Prefix(HashMap<AnimationPrefix, T>),
|
Prefix(HashMap<AnimationPrefix, T>),
|
||||||
|
/// Animation configuration for all animation kinds
|
||||||
Global(T),
|
Global(T),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use color_eyre::Result;
|
use color_eyre::eyre;
|
||||||
|
|
||||||
pub trait RenderDispatcher {
|
pub trait RenderDispatcher {
|
||||||
fn get_animation_key(&self) -> String;
|
fn get_animation_key(&self) -> String;
|
||||||
fn pre_render(&self) -> Result<()>;
|
fn pre_render(&self) -> eyre::Result<()>;
|
||||||
fn render(&self, delta: f64) -> Result<()>;
|
fn render(&self, delta: f64) -> eyre::Result<()>;
|
||||||
fn post_render(&self) -> Result<()>;
|
fn post_render(&self) -> eyre::Result<()>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -420,6 +420,7 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
|||||||
AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
|
AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
|
||||||
AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
|
AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
|
||||||
AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t),
|
AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t),
|
||||||
|
AnimationStyle::EaseOutCubic => EaseOutCubic::evaluate(t),
|
||||||
AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
|
AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
|
||||||
AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t),
|
AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t),
|
||||||
AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t),
|
AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t),
|
||||||
|
|||||||
@@ -1,33 +1,32 @@
|
|||||||
use crate::border_manager::window_kind_colour;
|
use crate::WINDOWS_11;
|
||||||
use crate::border_manager::RenderTarget;
|
use crate::WindowsApi;
|
||||||
use crate::border_manager::WindowKind;
|
|
||||||
use crate::border_manager::BORDER_OFFSET;
|
use crate::border_manager::BORDER_OFFSET;
|
||||||
use crate::border_manager::BORDER_WIDTH;
|
use crate::border_manager::BORDER_WIDTH;
|
||||||
|
use crate::border_manager::RenderTarget;
|
||||||
use crate::border_manager::STYLE;
|
use crate::border_manager::STYLE;
|
||||||
|
use crate::border_manager::WindowKind;
|
||||||
|
use crate::border_manager::window_kind_colour;
|
||||||
use crate::core::BorderStyle;
|
use crate::core::BorderStyle;
|
||||||
use crate::core::Rect;
|
use crate::core::Rect;
|
||||||
use crate::windows_api;
|
use crate::windows_api;
|
||||||
use crate::WindowsApi;
|
|
||||||
use crate::WINDOWS_11;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::sync::LazyLock;
|
|
||||||
use windows::Win32::Foundation::FALSE;
|
use windows::Win32::Foundation::FALSE;
|
||||||
use windows::Win32::Foundation::HWND;
|
use windows::Win32::Foundation::HWND;
|
||||||
use windows::Win32::Foundation::LPARAM;
|
use windows::Win32::Foundation::LPARAM;
|
||||||
use windows::Win32::Foundation::LRESULT;
|
use windows::Win32::Foundation::LRESULT;
|
||||||
use windows::Win32::Foundation::TRUE;
|
use windows::Win32::Foundation::TRUE;
|
||||||
use windows::Win32::Foundation::WPARAM;
|
use windows::Win32::Foundation::WPARAM;
|
||||||
|
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
||||||
|
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
||||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED;
|
use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED;
|
||||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
|
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
|
||||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT;
|
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT;
|
||||||
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
|
||||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
|
||||||
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
|
||||||
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
|
||||||
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
|
||||||
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
|
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
|
||||||
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
|
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
|
||||||
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
|
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
|
||||||
@@ -36,40 +35,49 @@ use windows::Win32::Graphics::Direct2D::D2D1_PRESENT_OPTIONS_IMMEDIATELY;
|
|||||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES;
|
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES;
|
||||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
|
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
|
||||||
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
|
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
|
||||||
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
|
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
||||||
|
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
||||||
|
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
||||||
use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
|
use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
|
||||||
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
|
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
|
||||||
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
|
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
|
||||||
|
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
|
||||||
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
|
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
|
||||||
use windows::Win32::Graphics::Gdi::CreateRectRgn;
|
use windows::Win32::Graphics::Gdi::CreateRectRgn;
|
||||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||||
use windows::Win32::Graphics::Gdi::ValidateRect;
|
use windows::Win32::Graphics::Gdi::ValidateRect;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
|
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
|
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
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::SM_CXVIRTUALSCREEN;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
|
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
|
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WM_USER;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||||
use windows_core::BOOL;
|
use windows_core::BOOL;
|
||||||
use windows_core::PCWSTR;
|
use windows_core::PCWSTR;
|
||||||
use windows_numerics::Matrix3x2;
|
use windows_numerics::Matrix3x2;
|
||||||
|
|
||||||
|
/// Custom WM_USER message that tells the border window thread to call update_brushes() on itself,
|
||||||
|
/// avoiding a data race between the border manager thread and the border's message loop thread.
|
||||||
|
pub const WM_UPDATE_BRUSHES: u32 = WM_USER + 1;
|
||||||
|
|
||||||
pub struct RenderFactory(ID2D1Factory);
|
pub struct RenderFactory(ID2D1Factory);
|
||||||
unsafe impl Sync for RenderFactory {}
|
unsafe impl Sync for RenderFactory {}
|
||||||
unsafe impl Send for RenderFactory {}
|
unsafe impl Send for RenderFactory {}
|
||||||
@@ -102,10 +110,10 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
|||||||
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||||
let hwnd = hwnd.0 as isize;
|
let hwnd = hwnd.0 as isize;
|
||||||
|
|
||||||
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
|
if let Ok(class) = WindowsApi::real_window_class_w(hwnd)
|
||||||
if class.starts_with("komoborder") {
|
&& class.starts_with("komoborder")
|
||||||
hwnds.push(hwnd);
|
{
|
||||||
}
|
hwnds.push(hwnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
true.into()
|
true.into()
|
||||||
@@ -126,6 +134,7 @@ pub struct Border {
|
|||||||
pub brush_properties: D2D1_BRUSH_PROPERTIES,
|
pub brush_properties: D2D1_BRUSH_PROPERTIES,
|
||||||
pub rounded_rect: D2D1_ROUNDED_RECT,
|
pub rounded_rect: D2D1_ROUNDED_RECT,
|
||||||
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
|
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
|
||||||
|
pub is_destroying: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<isize> for Border {
|
impl From<isize> for Border {
|
||||||
@@ -144,6 +153,7 @@ impl From<isize> for Border {
|
|||||||
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
|
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
|
||||||
rounded_rect: D2D1_ROUNDED_RECT::default(),
|
rounded_rect: D2D1_ROUNDED_RECT::default(),
|
||||||
brushes: HashMap::new(),
|
brushes: HashMap::new(),
|
||||||
|
is_destroying: Arc::new(AtomicBool::new(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,6 +202,7 @@ impl Border {
|
|||||||
brush_properties: Default::default(),
|
brush_properties: Default::default(),
|
||||||
rounded_rect: Default::default(),
|
rounded_rect: Default::default(),
|
||||||
brushes: HashMap::new(),
|
brushes: HashMap::new(),
|
||||||
|
is_destroying: Arc::new(AtomicBool::new(false)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let border_pointer = &raw mut border;
|
let border_pointer = &raw mut border;
|
||||||
@@ -313,9 +324,31 @@ impl Border {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// 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)
|
WindowsApi::close_window(self.hwnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Post a message to the border's own message loop thread requesting a brush update.
|
||||||
|
/// This ensures update_brushes() always runs on the window thread that owns the D2D
|
||||||
|
/// render target, preventing a data race with concurrent WndProc render operations.
|
||||||
|
pub fn request_brush_update(&self) {
|
||||||
|
let _ = unsafe {
|
||||||
|
PostMessageW(
|
||||||
|
Option::from(self.hwnd()),
|
||||||
|
WM_UPDATE_BRUSHES,
|
||||||
|
WPARAM(0),
|
||||||
|
LPARAM(0),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
|
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
|
||||||
let mut rect = *rect;
|
let mut rect = *rect;
|
||||||
rect.add_margin(self.width);
|
rect.add_margin(self.width);
|
||||||
@@ -381,6 +414,10 @@ impl Border {
|
|||||||
return LRESULT(0);
|
return LRESULT(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
|
||||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||||
|
|
||||||
let old_rect = (*border_pointer).window_rect;
|
let old_rect = (*border_pointer).window_rect;
|
||||||
@@ -392,63 +429,68 @@ impl Border {
|
|||||||
tracing::error!("failed to update border position {error}");
|
tracing::error!("failed to update border position {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect) {
|
if (!rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect))
|
||||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
&& let Some(render_target) = (*border_pointer).render_target.as_ref()
|
||||||
let border_width = (*border_pointer).width;
|
{
|
||||||
let border_offset = (*border_pointer).offset;
|
// double-check destruction flag before rendering
|
||||||
|
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
|
||||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
let border_width = (*border_pointer).width;
|
||||||
left: (border_width / 2 - border_offset) as f32,
|
let border_offset = (*border_pointer).offset;
|
||||||
top: (border_width / 2 - border_offset) as f32,
|
|
||||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
match style {
|
||||||
width: rect.right as u32,
|
BorderStyle::Rounded => {
|
||||||
height: rect.bottom as u32,
|
render_target.DrawRoundedRectangle(
|
||||||
});
|
&(*border_pointer).rounded_rect,
|
||||||
|
brush,
|
||||||
let window_kind = (*border_pointer).window_kind;
|
border_width as f32,
|
||||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
None,
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
BorderStyle::Square => {
|
||||||
let _ = render_target.EndDraw(None, None);
|
render_target.DrawRectangle(
|
||||||
|
&(*border_pointer).rounded_rect.rect,
|
||||||
|
brush,
|
||||||
|
border_width as f32,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _ = render_target.EndDraw(None, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +505,10 @@ impl Border {
|
|||||||
return LRESULT(0);
|
return LRESULT(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
|
||||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||||
|
|
||||||
// Update position to update the ZOrder
|
// Update position to update the ZOrder
|
||||||
@@ -476,6 +522,11 @@ impl Border {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
if 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);
|
||||||
|
}
|
||||||
|
|
||||||
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
||||||
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
||||||
|
|
||||||
@@ -542,8 +593,27 @@ impl Border {
|
|||||||
let _ = ValidateRect(Option::from(window), None);
|
let _ = ValidateRect(Option::from(window), None);
|
||||||
LRESULT(0)
|
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 => {
|
WM_DESTROY => {
|
||||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||||
|
if !border_pointer.is_null() {
|
||||||
|
(*border_pointer).render_target = None;
|
||||||
|
(*border_pointer).brushes.clear();
|
||||||
|
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||||
|
}
|
||||||
PostQuitMessage(0);
|
PostQuitMessage(0);
|
||||||
LRESULT(0)
|
LRESULT(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
mod border;
|
mod border;
|
||||||
|
use crate::WindowManager;
|
||||||
|
use crate::WindowsApi;
|
||||||
use crate::core::BorderImplementation;
|
use crate::core::BorderImplementation;
|
||||||
use crate::core::BorderStyle;
|
use crate::core::BorderStyle;
|
||||||
use crate::core::WindowKind;
|
use crate::core::WindowKind;
|
||||||
@@ -8,10 +10,8 @@ use crate::ring::Ring;
|
|||||||
use crate::windows_api;
|
use crate::windows_api;
|
||||||
use crate::workspace::Workspace;
|
use crate::workspace::Workspace;
|
||||||
use crate::workspace::WorkspaceLayer;
|
use crate::workspace::WorkspaceLayer;
|
||||||
use crate::WindowManager;
|
|
||||||
use crate::WindowsApi;
|
|
||||||
use border::border_hwnds;
|
|
||||||
pub use border::Border;
|
pub use border::Border;
|
||||||
|
use border::border_hwnds;
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use crossbeam_utils::atomic::AtomicCell;
|
use crossbeam_utils::atomic::AtomicCell;
|
||||||
@@ -22,15 +22,15 @@ use lazy_static::lazy_static;
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::OnceLock;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::atomic::AtomicI32;
|
use std::sync::atomic::AtomicI32;
|
||||||
use std::sync::atomic::AtomicU32;
|
use std::sync::atomic::AtomicU32;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
use windows::Win32::Foundation::HWND;
|
use windows::Win32::Foundation::HWND;
|
||||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||||
@@ -170,13 +170,15 @@ fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||||
std::thread::spawn(move || loop {
|
std::thread::spawn(move || {
|
||||||
match handle_notifications(wm.clone()) {
|
loop {
|
||||||
Ok(()) => {
|
match handle_notifications(wm.clone()) {
|
||||||
tracing::warn!("restarting finished thread");
|
Ok(()) => {
|
||||||
}
|
tracing::warn!("restarting finished thread");
|
||||||
Err(error) => {
|
}
|
||||||
tracing::warn!("restarting failed thread: {}", error);
|
Err(error) => {
|
||||||
|
tracing::warn!("restarting failed thread: {}", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -209,9 +211,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|w| w.hwnd)
|
.map(|w| w.hwnd)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let workspace_layer = *state.monitors.elements()[focused_monitor_idx].workspaces()
|
let workspace_layer = state.monitors.elements()[focused_monitor_idx].workspaces()
|
||||||
[focused_workspace_idx]
|
[focused_workspace_idx]
|
||||||
.layer();
|
.layer;
|
||||||
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
|
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
|
||||||
let layer_changed = previous_layer != workspace_layer;
|
let layer_changed = previous_layer != workspace_layer;
|
||||||
let forced_update = matches!(notification, Notification::ForceUpdate);
|
let forced_update = matches!(notification, Notification::ForceUpdate);
|
||||||
@@ -224,7 +226,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
// Only operate on the focused workspace of each monitor
|
// Only operate on the focused workspace of each monitor
|
||||||
if let Some(ws) = m.focused_workspace() {
|
if let Some(ws) = m.focused_workspace() {
|
||||||
// Handle the monocle container separately
|
// Handle the monocle container separately
|
||||||
if let Some(monocle) = ws.monocle_container() {
|
if let Some(monocle) = &ws.monocle_container {
|
||||||
let window_kind = if monitor_idx != focused_monitor_idx {
|
let window_kind = if monitor_idx != focused_monitor_idx {
|
||||||
WindowKind::Unfocused
|
WindowKind::Unfocused
|
||||||
} else {
|
} else {
|
||||||
@@ -237,7 +239,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.set_accent(window_kind_colour(window_kind))?;
|
.set_accent(window_kind_colour(window_kind))?;
|
||||||
|
|
||||||
if ws.layer() == &WorkspaceLayer::Floating {
|
if ws.layer == WorkspaceLayer::Floating {
|
||||||
for window in ws.floating_windows() {
|
for window in ws.floating_windows() {
|
||||||
let mut window_kind = WindowKind::Unfocused;
|
let mut window_kind = WindowKind::Unfocused;
|
||||||
|
|
||||||
@@ -255,7 +257,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
let window_kind = if idx != ws.focused_container_idx()
|
let window_kind = if idx != ws.focused_container_idx()
|
||||||
|| monitor_idx != focused_monitor_idx
|
|| monitor_idx != focused_monitor_idx
|
||||||
{
|
{
|
||||||
if c.locked() {
|
if c.locked {
|
||||||
WindowKind::UnfocusedLocked
|
WindowKind::UnfocusedLocked
|
||||||
} else {
|
} else {
|
||||||
WindowKind::Unfocused
|
WindowKind::Unfocused
|
||||||
@@ -339,15 +341,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
should_process_notification = true;
|
should_process_notification = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !should_process_notification {
|
if !should_process_notification
|
||||||
if let Some(Notification::Update(ref previous)) = previous_notification
|
&& let Some(Notification::Update(ref previous)) = previous_notification
|
||||||
{
|
&& previous.unwrap_or_default() != notification_hwnd.unwrap_or_default()
|
||||||
if previous.unwrap_or_default()
|
{
|
||||||
!= notification_hwnd.unwrap_or_default()
|
should_process_notification = true;
|
||||||
{
|
|
||||||
should_process_notification = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
should_process_notification
|
should_process_notification
|
||||||
@@ -383,7 +381,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
// Only operate on the focused workspace of each monitor
|
// Only operate on the focused workspace of each monitor
|
||||||
if let Some(ws) = m.focused_workspace() {
|
if let Some(ws) = m.focused_workspace() {
|
||||||
// Workspaces with tiling disabled don't have borders
|
// Workspaces with tiling disabled don't have borders
|
||||||
if !ws.tile() {
|
if !ws.tile {
|
||||||
// Remove all borders on this monitor
|
// Remove all borders on this monitor
|
||||||
remove_borders(
|
remove_borders(
|
||||||
&mut borders,
|
&mut borders,
|
||||||
@@ -396,16 +394,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle the monocle container separately
|
// Handle the monocle container separately
|
||||||
if let Some(monocle) = ws.monocle_container() {
|
if let Some(monocle) = &ws.monocle_container {
|
||||||
let mut new_border = false;
|
let mut new_border = false;
|
||||||
let focused_window_hwnd =
|
let focused_window_hwnd =
|
||||||
monocle.focused_window().map(|w| w.hwnd).unwrap_or_default();
|
monocle.focused_window().map(|w| w.hwnd).unwrap_or_default();
|
||||||
let id = monocle.id().clone();
|
let id = monocle.id.clone();
|
||||||
let border = match borders.entry(id.clone()) {
|
let border = match borders.entry(id.clone()) {
|
||||||
Entry::Occupied(entry) => entry.into_mut(),
|
Entry::Occupied(entry) => entry.into_mut(),
|
||||||
Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
if let Ok(border) = Border::create(
|
if let Ok(border) = Border::create(
|
||||||
monocle.id(),
|
&monocle.id,
|
||||||
focused_window_hwnd,
|
focused_window_hwnd,
|
||||||
monitor_idx,
|
monitor_idx,
|
||||||
) {
|
) {
|
||||||
@@ -453,8 +451,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
} else if matches!(notification, Notification::ForceUpdate) {
|
} else if matches!(notification, Notification::ForceUpdate) {
|
||||||
// Update the border brushes if there was a forced update
|
// Update the border brushes if there was a forced update
|
||||||
// notification and this is not a new border (new border's
|
// notification and this is not a new border (new border's
|
||||||
// already have their brushes updated on creation)
|
// already have their brushes updated on creation).
|
||||||
border.update_brushes()?;
|
// 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();
|
border.invalidate();
|
||||||
@@ -463,7 +464,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
|
|
||||||
let border_hwnd = border.hwnd;
|
let border_hwnd = border.hwnd;
|
||||||
|
|
||||||
if ws.layer() == &WorkspaceLayer::Floating {
|
if ws.layer == WorkspaceLayer::Floating {
|
||||||
handle_floating_borders(
|
handle_floating_borders(
|
||||||
&mut borders,
|
&mut borders,
|
||||||
&mut windows_borders,
|
&mut windows_borders,
|
||||||
@@ -502,8 +503,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
|
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
|
||||||
let foreground_monitor_id =
|
let foreground_monitor_id =
|
||||||
WindowsApi::monitor_from_window(foreground_hwnd);
|
WindowsApi::monitor_from_window(foreground_hwnd);
|
||||||
let is_maximized = foreground_monitor_id == m.id()
|
let is_maximized =
|
||||||
&& WindowsApi::is_zoomed(foreground_hwnd);
|
foreground_monitor_id == m.id && WindowsApi::is_zoomed(foreground_hwnd);
|
||||||
|
|
||||||
if is_maximized {
|
if is_maximized {
|
||||||
// Remove all borders on this monitor
|
// Remove all borders on this monitor
|
||||||
@@ -521,7 +522,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
let mut container_and_floating_window_ids = ws
|
let mut container_and_floating_window_ids = ws
|
||||||
.containers()
|
.containers()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.id().clone())
|
.map(|c| c.id.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for w in ws.floating_windows() {
|
for w in ws.floating_windows() {
|
||||||
@@ -539,7 +540,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
'containers: for (idx, c) in ws.containers().iter().enumerate() {
|
'containers: for (idx, c) in ws.containers().iter().enumerate() {
|
||||||
let focused_window_hwnd =
|
let focused_window_hwnd =
|
||||||
c.focused_window().map(|w| w.hwnd).unwrap_or_default();
|
c.focused_window().map(|w| w.hwnd).unwrap_or_default();
|
||||||
let id = c.id().clone();
|
let id = c.id.clone();
|
||||||
|
|
||||||
// Get the border entry for this container from the map or create one
|
// Get the border entry for this container from the map or create one
|
||||||
let mut new_border = false;
|
let mut new_border = false;
|
||||||
@@ -547,7 +548,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
Entry::Occupied(entry) => entry.into_mut(),
|
Entry::Occupied(entry) => entry.into_mut(),
|
||||||
Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
if let Ok(border) =
|
if let Ok(border) =
|
||||||
Border::create(c.id(), focused_window_hwnd, monitor_idx)
|
Border::create(&c.id, focused_window_hwnd, monitor_idx)
|
||||||
{
|
{
|
||||||
new_border = true;
|
new_border = true;
|
||||||
entry.insert(border)
|
entry.insert(border)
|
||||||
@@ -563,7 +564,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
|| monitor_idx != focused_monitor_idx
|
|| monitor_idx != focused_monitor_idx
|
||||||
|| focused_window_hwnd != foreground_window
|
|| focused_window_hwnd != foreground_window
|
||||||
{
|
{
|
||||||
if c.locked() {
|
if c.locked {
|
||||||
WindowKind::UnfocusedLocked
|
WindowKind::UnfocusedLocked
|
||||||
} else {
|
} else {
|
||||||
WindowKind::Unfocused
|
WindowKind::Unfocused
|
||||||
@@ -603,7 +604,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
let rect = match WindowsApi::window_rect(focused_window_hwnd) {
|
let rect = match WindowsApi::window_rect(focused_window_hwnd) {
|
||||||
Ok(rect) => rect,
|
Ok(rect) => rect,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
remove_border(c.id(), &mut borders, &mut windows_borders)?;
|
remove_border(&c.id, &mut borders, &mut windows_borders)?;
|
||||||
continue 'containers;
|
continue 'containers;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -618,8 +619,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
if forced_update && !new_border {
|
if forced_update && !new_border {
|
||||||
// Update the border brushes if there was a forced update
|
// Update the border brushes if there was a forced update
|
||||||
// notification and this is not a new border (new border's
|
// notification and this is not a new border (new border's
|
||||||
// already have their brushes updated on creation)
|
// already have their brushes updated on creation).
|
||||||
border.update_brushes()?;
|
// 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.set_position(&rect, focused_window_hwnd)?;
|
||||||
border.invalidate();
|
border.invalidate();
|
||||||
@@ -701,8 +705,11 @@ fn handle_floating_borders(
|
|||||||
if forced_update && !new_border {
|
if forced_update && !new_border {
|
||||||
// Update the border brushes if there was a forced update
|
// Update the border brushes if there was a forced update
|
||||||
// notification and this is not a new border (new border's
|
// notification and this is not a new border (new border's
|
||||||
// already have their brushes updated on creation)
|
// already have their brushes updated on creation).
|
||||||
border.update_brushes()?;
|
// 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.set_position(&rect, window.hwnd)?;
|
||||||
border.invalidate();
|
border.invalidate();
|
||||||
@@ -769,6 +776,7 @@ fn remove_border(
|
|||||||
fn destroy_border(border: Box<Border>) -> color_eyre::Result<()> {
|
fn destroy_border(border: Box<Border>) -> color_eyre::Result<()> {
|
||||||
let raw_pointer = Box::into_raw(border);
|
let raw_pointer = Box::into_raw(border);
|
||||||
unsafe {
|
unsafe {
|
||||||
|
// Now safe to destroy window
|
||||||
(*raw_pointer).destroy()?;
|
(*raw_pointer).destroy()?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -815,10 +823,26 @@ pub fn hide_border(tracking_hwnd: isize) {
|
|||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Z Order (https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos)
|
||||||
pub enum ZOrder {
|
pub enum ZOrder {
|
||||||
|
/// HWND_TOP
|
||||||
|
///
|
||||||
|
/// Places the window at the top of the Z order.
|
||||||
Top,
|
Top,
|
||||||
|
/// HWND_NOTOPMOST
|
||||||
|
///
|
||||||
|
/// Places the window above all non-topmost windows (that is, behind all topmost windows).
|
||||||
|
/// This flag has no effect if the window is already a non-topmost window.
|
||||||
NoTopMost,
|
NoTopMost,
|
||||||
|
/// HWND_BOTTOM
|
||||||
|
///
|
||||||
|
/// Places the window at the bottom of the Z order. If the hWnd parameter identifies a topmost window,
|
||||||
|
/// the window loses its topmost status and is placed at the bottom of all other windows.
|
||||||
Bottom,
|
Bottom,
|
||||||
|
/// HWND_TOPMOST
|
||||||
|
///
|
||||||
|
/// Places the window above all non-topmost windows.
|
||||||
|
/// The window maintains its topmost position even when it is deactivated.
|
||||||
TopMost,
|
TopMost,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
|
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use windows::core::IUnknown;
|
|
||||||
use windows::core::IUnknown_Vtbl;
|
|
||||||
use windows::core::GUID;
|
|
||||||
use windows::core::HRESULT;
|
|
||||||
use windows::core::HSTRING;
|
|
||||||
use windows::core::PCWSTR;
|
|
||||||
use windows::core::PWSTR;
|
|
||||||
use windows::Win32::Foundation::HWND;
|
use windows::Win32::Foundation::HWND;
|
||||||
use windows::Win32::Foundation::RECT;
|
use windows::Win32::Foundation::RECT;
|
||||||
use windows::Win32::Foundation::SIZE;
|
use windows::Win32::Foundation::SIZE;
|
||||||
use windows::Win32::UI::Shell::Common::IObjectArray;
|
use windows::Win32::UI::Shell::Common::IObjectArray;
|
||||||
|
use windows::core::GUID;
|
||||||
|
use windows::core::HRESULT;
|
||||||
|
use windows::core::HSTRING;
|
||||||
|
use windows::core::IUnknown;
|
||||||
|
use windows::core::IUnknown_Vtbl;
|
||||||
|
use windows::core::PCWSTR;
|
||||||
|
use windows::core::PWSTR;
|
||||||
use windows_core::BOOL;
|
use windows_core::BOOL;
|
||||||
|
|
||||||
type DesktopID = GUID;
|
type DesktopID = GUID;
|
||||||
@@ -129,7 +129,7 @@ pub unsafe trait IApplicationView: IUnknown {
|
|||||||
pub unsafe fn get_app_user_model_id(&self, id: *mut PWSTR) -> HRESULT; // Proc17
|
pub unsafe fn get_app_user_model_id(&self, id: *mut PWSTR) -> HRESULT; // Proc17
|
||||||
pub unsafe fn set_app_user_model_id(&self, id: PCWSTR) -> HRESULT;
|
pub unsafe fn set_app_user_model_id(&self, id: PCWSTR) -> HRESULT;
|
||||||
pub unsafe fn is_equal_by_app_user_model_id(&self, id: PCWSTR, out_result: *mut INT)
|
pub unsafe fn is_equal_by_app_user_model_id(&self, id: PCWSTR, out_result: *mut INT)
|
||||||
-> HRESULT;
|
-> HRESULT;
|
||||||
|
|
||||||
/*** IApplicationView methods ***/
|
/*** IApplicationView methods ***/
|
||||||
pub unsafe fn get_view_state(&self, out_state: *mut UINT) -> HRESULT; // Proc20
|
pub unsafe fn get_view_state(&self, out_state: *mut UINT) -> HRESULT; // Proc20
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ use interfaces::IServiceProvider;
|
|||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
|
|
||||||
use windows::Win32::Foundation::HWND;
|
use windows::Win32::Foundation::HWND;
|
||||||
|
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||||
|
use windows::Win32::System::Com::COINIT_MULTITHREADED;
|
||||||
use windows::Win32::System::Com::CoCreateInstance;
|
use windows::Win32::System::Com::CoCreateInstance;
|
||||||
use windows::Win32::System::Com::CoInitializeEx;
|
use windows::Win32::System::Com::CoInitializeEx;
|
||||||
use windows::Win32::System::Com::CoUninitialize;
|
use windows::Win32::System::Com::CoUninitialize;
|
||||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
|
||||||
use windows::Win32::System::Com::COINIT_MULTITHREADED;
|
|
||||||
use windows_core::Interface;
|
use windows_core::Interface;
|
||||||
|
|
||||||
struct ComInit();
|
struct ComInit();
|
||||||
@@ -64,7 +64,7 @@ fn get_iapplication_view_collection(provider: &IServiceProvider) -> IApplication
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
|
pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
|
||||||
COM_INIT.with(|_| {
|
COM_INIT.with(|_| {
|
||||||
let provider = get_iservice_provider();
|
let provider = get_iservice_provider();
|
||||||
|
|||||||
+35
-29
@@ -1,24 +1,19 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use getset::CopyGetters;
|
|
||||||
use getset::Getters;
|
|
||||||
use getset::Setters;
|
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::Lockable;
|
||||||
use crate::ring::Ring;
|
use crate::ring::Ring;
|
||||||
use crate::window::Window;
|
use crate::window::Window;
|
||||||
use crate::Lockable;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, CopyGetters, Setters)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
#[getset(get = "pub")]
|
pub id: String,
|
||||||
id: String,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
pub locked: bool,
|
||||||
locked: bool,
|
|
||||||
windows: Ring<Window>,
|
windows: Ring<Window>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,16 +41,27 @@ impl Lockable for Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Container {
|
impl Container {
|
||||||
|
pub fn preselect() -> Self {
|
||||||
|
Self {
|
||||||
|
id: "PRESELECT".to_string(),
|
||||||
|
locked: false,
|
||||||
|
windows: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_preselect(&self) -> bool {
|
||||||
|
self.id == "PRESELECT"
|
||||||
|
}
|
||||||
|
|
||||||
pub fn hide(&self, omit: Option<isize>) {
|
pub fn hide(&self, omit: Option<isize>) {
|
||||||
for window in self.windows().iter().rev() {
|
for window in self.windows().iter().rev() {
|
||||||
let mut should_hide = omit.is_none();
|
let mut should_hide = omit.is_none();
|
||||||
|
|
||||||
if !should_hide {
|
if !should_hide
|
||||||
if let Some(omit) = omit {
|
&& let Some(omit) = omit
|
||||||
if omit != window.hwnd {
|
&& omit != window.hwnd
|
||||||
should_hide = true
|
{
|
||||||
}
|
should_hide = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if should_hide {
|
if should_hide {
|
||||||
@@ -87,10 +93,10 @@ impl Container {
|
|||||||
|
|
||||||
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
|
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
|
||||||
for window in self.windows() {
|
for window in self.windows() {
|
||||||
if let Ok(window_exe) = window.exe() {
|
if let Ok(window_exe) = window.exe()
|
||||||
if exe == window_exe {
|
&& exe == window_exe
|
||||||
return Option::from(window.hwnd);
|
{
|
||||||
}
|
return Option::from(window.hwnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,10 +105,10 @@ impl Container {
|
|||||||
|
|
||||||
pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
|
pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
|
||||||
for (idx, window) in self.windows().iter().enumerate() {
|
for (idx, window) in self.windows().iter().enumerate() {
|
||||||
if let Ok(window_exe) = window.exe() {
|
if let Ok(window_exe) = window.exe()
|
||||||
if exe == window_exe {
|
&& exe == window_exe
|
||||||
return Option::from(idx);
|
{
|
||||||
}
|
return Option::from(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,8 +284,8 @@ mod tests {
|
|||||||
}"#;
|
}"#;
|
||||||
let container: Container = serde_json::from_str(json).expect("Should deserialize");
|
let container: Container = serde_json::from_str(json).expect("Should deserialize");
|
||||||
|
|
||||||
assert!(!container.locked());
|
assert!(!container.locked);
|
||||||
assert_eq!(container.id(), "test-1");
|
assert_eq!(container.id, "test-1");
|
||||||
assert!(container.windows().is_empty());
|
assert!(container.windows().is_empty());
|
||||||
|
|
||||||
let json = r#"{
|
let json = r#"{
|
||||||
@@ -287,8 +293,8 @@ mod tests {
|
|||||||
"windows": { "elements": [ { "hwnd": 5 }, { "hwnd": 9 } ], "focused": 1 }
|
"windows": { "elements": [ { "hwnd": 5 }, { "hwnd": 9 } ], "focused": 1 }
|
||||||
}"#;
|
}"#;
|
||||||
let container: Container = serde_json::from_str(json).unwrap();
|
let container: Container = serde_json::from_str(json).unwrap();
|
||||||
assert_eq!(container.id(), "test-2");
|
assert_eq!(container.id, "test-2");
|
||||||
assert!(!container.locked());
|
assert!(!container.locked);
|
||||||
assert_eq!(container.windows(), &[Window::from(5), Window::from(9)]);
|
assert_eq!(container.windows(), &[Window::from(5), Window::from(9)]);
|
||||||
assert_eq!(container.focused_window_idx(), 1);
|
assert_eq!(container.focused_window_idx(), 1);
|
||||||
}
|
}
|
||||||
@@ -302,7 +308,7 @@ mod tests {
|
|||||||
let deserialized: Container =
|
let deserialized: Container =
|
||||||
serde_json::from_str(&serialized).expect("Should deserialize");
|
serde_json::from_str(&serialized).expect("Should deserialize");
|
||||||
|
|
||||||
assert!(deserialized.locked());
|
assert!(deserialized.locked);
|
||||||
assert_eq!(deserialized.id(), container.id());
|
assert_eq!(deserialized.id, container.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,80 @@
|
|||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
|
|
||||||
use serde::ser::SerializeSeq;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use serde::ser::SerializeSeq;
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Display, EnumString, ValueEnum, PartialEq)]
|
#[derive(Copy, Clone, Debug, Display, EnumString, ValueEnum, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Mathematical function which describes the rate at which a value changes
|
||||||
pub enum AnimationStyle {
|
pub enum AnimationStyle {
|
||||||
|
/// Linear
|
||||||
Linear,
|
Linear,
|
||||||
|
/// Ease in sine
|
||||||
EaseInSine,
|
EaseInSine,
|
||||||
|
/// Ease out sine
|
||||||
EaseOutSine,
|
EaseOutSine,
|
||||||
|
/// Ease in out sine
|
||||||
EaseInOutSine,
|
EaseInOutSine,
|
||||||
|
/// Ease in quad
|
||||||
EaseInQuad,
|
EaseInQuad,
|
||||||
|
/// Ease out quad
|
||||||
EaseOutQuad,
|
EaseOutQuad,
|
||||||
|
/// Ease in out quad
|
||||||
EaseInOutQuad,
|
EaseInOutQuad,
|
||||||
|
/// Ease in cubic
|
||||||
EaseInCubic,
|
EaseInCubic,
|
||||||
|
/// Ease out cubic
|
||||||
|
EaseOutCubic,
|
||||||
|
/// Ease in out cubic
|
||||||
EaseInOutCubic,
|
EaseInOutCubic,
|
||||||
|
/// Ease in quart
|
||||||
EaseInQuart,
|
EaseInQuart,
|
||||||
|
/// Ease out quart
|
||||||
EaseOutQuart,
|
EaseOutQuart,
|
||||||
|
/// Ease in out quart
|
||||||
EaseInOutQuart,
|
EaseInOutQuart,
|
||||||
|
/// Ease in quint
|
||||||
EaseInQuint,
|
EaseInQuint,
|
||||||
|
/// Ease out quint
|
||||||
EaseOutQuint,
|
EaseOutQuint,
|
||||||
|
/// Ease in out quint
|
||||||
EaseInOutQuint,
|
EaseInOutQuint,
|
||||||
|
/// Ease in expo
|
||||||
EaseInExpo,
|
EaseInExpo,
|
||||||
|
/// Ease out expo
|
||||||
EaseOutExpo,
|
EaseOutExpo,
|
||||||
|
/// Ease in out expo
|
||||||
EaseInOutExpo,
|
EaseInOutExpo,
|
||||||
|
/// Ease in circ
|
||||||
EaseInCirc,
|
EaseInCirc,
|
||||||
|
/// Ease out circ
|
||||||
EaseOutCirc,
|
EaseOutCirc,
|
||||||
|
/// Ease in out circ
|
||||||
EaseInOutCirc,
|
EaseInOutCirc,
|
||||||
|
/// Ease in back
|
||||||
EaseInBack,
|
EaseInBack,
|
||||||
|
/// Ease out back
|
||||||
EaseOutBack,
|
EaseOutBack,
|
||||||
|
/// Ease in out back
|
||||||
EaseInOutBack,
|
EaseInOutBack,
|
||||||
|
/// Ease in elastic
|
||||||
EaseInElastic,
|
EaseInElastic,
|
||||||
|
/// Ease out elastic
|
||||||
EaseOutElastic,
|
EaseOutElastic,
|
||||||
|
/// Ease in out elastic
|
||||||
EaseInOutElastic,
|
EaseInOutElastic,
|
||||||
|
/// Ease in bounce
|
||||||
EaseInBounce,
|
EaseInBounce,
|
||||||
|
/// Ease out bounce
|
||||||
EaseOutBounce,
|
EaseOutBounce,
|
||||||
|
/// Ease in out bounce
|
||||||
EaseInOutBounce,
|
EaseInOutBounce,
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(title = "CubicBezier"))]
|
||||||
#[value(skip)]
|
#[value(skip)]
|
||||||
|
/// Custom Cubic Bezier function
|
||||||
CubicBezier(f64, f64, f64, f64),
|
CubicBezier(f64, f64, f64, f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::config_generation::ApplicationConfiguration;
|
use crate::config_generation::ApplicationConfiguration;
|
||||||
use crate::config_generation::ApplicationOptions;
|
use crate::config_generation::ApplicationOptions;
|
||||||
use crate::config_generation::MatchingRule;
|
use crate::config_generation::MatchingRule;
|
||||||
use color_eyre::Result;
|
use color_eyre::eyre;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -36,12 +36,12 @@ impl DerefMut for ApplicationSpecificConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ApplicationSpecificConfiguration {
|
impl ApplicationSpecificConfiguration {
|
||||||
pub fn load(pathbuf: &PathBuf) -> Result<Self> {
|
pub fn load(pathbuf: &PathBuf) -> eyre::Result<Self> {
|
||||||
let content = std::fs::read_to_string(pathbuf)?;
|
let content = std::fs::read_to_string(pathbuf)?;
|
||||||
Ok(serde_json::from_str(&content)?)
|
Ok(serde_json::from_str(&content)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format(pathbuf: &PathBuf) -> Result<String> {
|
pub fn format(pathbuf: &PathBuf) -> eyre::Result<String> {
|
||||||
Ok(serde_json::to_string_pretty(&Self::load(pathbuf)?)?)
|
Ok(serde_json::to_string_pretty(&Self::load(pathbuf)?)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use color_eyre::Result;
|
use color_eyre::eyre;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
@@ -53,41 +53,64 @@ impl ApplicationOptions {
|
|||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
/// Rule for matching applications
|
||||||
pub enum MatchingRule {
|
pub enum MatchingRule {
|
||||||
|
/// Simple matching rule which must evaluate to true
|
||||||
Simple(IdWithIdentifier),
|
Simple(IdWithIdentifier),
|
||||||
|
/// Composite matching rule where all conditions must evaluate to true
|
||||||
Composite(Vec<IdWithIdentifier>),
|
Composite(Vec<IdWithIdentifier>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Rule for assigning applications to a workspace
|
||||||
pub struct WorkspaceMatchingRule {
|
pub struct WorkspaceMatchingRule {
|
||||||
|
/// Target monitor index
|
||||||
pub monitor_index: usize,
|
pub monitor_index: usize,
|
||||||
|
/// Target workspace index
|
||||||
pub workspace_index: usize,
|
pub workspace_index: usize,
|
||||||
|
/// Matching rule for the application
|
||||||
pub matching_rule: MatchingRule,
|
pub matching_rule: MatchingRule,
|
||||||
|
/// Whether to apply the rule only when the application is initially launched
|
||||||
pub initial_only: bool,
|
pub initial_only: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Rule for matching applications
|
||||||
pub struct IdWithIdentifier {
|
pub struct IdWithIdentifier {
|
||||||
|
/// Kind of identifier to target
|
||||||
pub kind: ApplicationIdentifier,
|
pub kind: ApplicationIdentifier,
|
||||||
|
/// Target identifier
|
||||||
pub id: String,
|
pub id: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Matching strategy to use
|
||||||
pub matching_strategy: Option<MatchingStrategy>,
|
pub matching_strategy: Option<MatchingStrategy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Strategy for matching identifiers
|
||||||
pub enum MatchingStrategy {
|
pub enum MatchingStrategy {
|
||||||
|
/// Should not be used, only kept for backward compatibility
|
||||||
Legacy,
|
Legacy,
|
||||||
|
/// Equals
|
||||||
Equals,
|
Equals,
|
||||||
|
/// Starts With
|
||||||
StartsWith,
|
StartsWith,
|
||||||
|
/// Ends With
|
||||||
EndsWith,
|
EndsWith,
|
||||||
|
/// Contains
|
||||||
Contains,
|
Contains,
|
||||||
|
/// Regex
|
||||||
Regex,
|
Regex,
|
||||||
|
/// Does not end with
|
||||||
DoesNotEndWith,
|
DoesNotEndWith,
|
||||||
|
/// Does not start with
|
||||||
DoesNotStartWith,
|
DoesNotStartWith,
|
||||||
|
/// Does not equal
|
||||||
DoesNotEqual,
|
DoesNotEqual,
|
||||||
|
/// Does not contain
|
||||||
DoesNotContain,
|
DoesNotContain,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,11 +165,11 @@ impl ApplicationConfiguration {
|
|||||||
pub struct ApplicationConfigurationGenerator;
|
pub struct ApplicationConfigurationGenerator;
|
||||||
|
|
||||||
impl ApplicationConfigurationGenerator {
|
impl ApplicationConfigurationGenerator {
|
||||||
pub fn load(content: &str) -> Result<Vec<ApplicationConfiguration>> {
|
pub fn load(content: &str) -> eyre::Result<Vec<ApplicationConfiguration>> {
|
||||||
Ok(serde_yaml::from_str(content)?)
|
Ok(serde_yaml::from_str(content)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format(content: &str) -> Result<String> {
|
pub fn format(content: &str) -> eyre::Result<String> {
|
||||||
let mut cfgen = Self::load(content)?;
|
let mut cfgen = Self::load(content)?;
|
||||||
for cfg in &mut cfgen {
|
for cfg in &mut cfgen {
|
||||||
cfg.populate_default_matching_strategies();
|
cfg.populate_default_matching_strategies();
|
||||||
@@ -156,7 +179,10 @@ impl ApplicationConfigurationGenerator {
|
|||||||
Ok(serde_yaml::to_string(&cfgen)?)
|
Ok(serde_yaml::to_string(&cfgen)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge(base_content: &str, override_content: &str) -> Result<Vec<ApplicationConfiguration>> {
|
fn merge(
|
||||||
|
base_content: &str,
|
||||||
|
override_content: &str,
|
||||||
|
) -> eyre::Result<Vec<ApplicationConfiguration>> {
|
||||||
let base_cfgen = Self::load(base_content)?;
|
let base_cfgen = Self::load(base_content)?;
|
||||||
let override_cfgen = Self::load(override_content)?;
|
let override_cfgen = Self::load(override_content)?;
|
||||||
|
|
||||||
@@ -182,7 +208,7 @@ impl ApplicationConfigurationGenerator {
|
|||||||
pub fn generate_pwsh(
|
pub fn generate_pwsh(
|
||||||
base_content: &str,
|
base_content: &str,
|
||||||
override_content: Option<&str>,
|
override_content: Option<&str>,
|
||||||
) -> Result<Vec<String>> {
|
) -> eyre::Result<Vec<String>> {
|
||||||
let mut cfgen = if let Some(override_content) = override_content {
|
let mut cfgen = if let Some(override_content) = override_content {
|
||||||
Self::merge(base_content, override_content)?
|
Self::merge(base_content, override_content)?
|
||||||
} else {
|
} else {
|
||||||
@@ -233,7 +259,10 @@ impl ApplicationConfigurationGenerator {
|
|||||||
Ok(lines)
|
Ok(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_ahk(base_content: &str, override_content: Option<&str>) -> Result<Vec<String>> {
|
pub fn generate_ahk(
|
||||||
|
base_content: &str,
|
||||||
|
override_content: Option<&str>,
|
||||||
|
) -> eyre::Result<Vec<String>> {
|
||||||
let mut cfgen = if let Some(override_content) = override_content {
|
let mut cfgen = if let Some(override_content) = override_content {
|
||||||
Self::merge(base_content, override_content)?
|
Self::merge(base_content, override_content)?
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
use clap::ValueEnum;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
use strum::Display;
|
|
||||||
use strum::EnumString;
|
|
||||||
|
|
||||||
use super::OperationDirection;
|
|
||||||
use super::Rect;
|
|
||||||
use super::Sizing;
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
|
|
||||||
)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
pub enum DefaultLayout {
|
|
||||||
BSP,
|
|
||||||
Columns,
|
|
||||||
Rows,
|
|
||||||
VerticalStack,
|
|
||||||
HorizontalStack,
|
|
||||||
UltrawideVerticalStack,
|
|
||||||
Grid,
|
|
||||||
RightMainVerticalStack,
|
|
||||||
Scrolling,
|
|
||||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
pub struct LayoutOptions {
|
|
||||||
/// Options related to the Scrolling layout
|
|
||||||
pub scrolling: Option<ScrollingLayoutOptions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
pub struct ScrollingLayoutOptions {
|
|
||||||
/// Desired number of visible columns (default: 3)
|
|
||||||
pub columns: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DefaultLayout {
|
|
||||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
|
||||||
match self {
|
|
||||||
Self::UltrawideVerticalStack | Self::RightMainVerticalStack => match len {
|
|
||||||
n if n > 1 => 1,
|
|
||||||
_ => 0,
|
|
||||||
},
|
|
||||||
Self::Scrolling => 0,
|
|
||||||
DefaultLayout::BSP
|
|
||||||
| DefaultLayout::Columns
|
|
||||||
| DefaultLayout::Rows
|
|
||||||
| DefaultLayout::VerticalStack
|
|
||||||
| DefaultLayout::HorizontalStack
|
|
||||||
| DefaultLayout::Grid => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rightmost_index(&self, len: usize) -> usize {
|
|
||||||
match self {
|
|
||||||
DefaultLayout::BSP
|
|
||||||
| DefaultLayout::Columns
|
|
||||||
| DefaultLayout::Rows
|
|
||||||
| DefaultLayout::VerticalStack
|
|
||||||
| DefaultLayout::HorizontalStack
|
|
||||||
| DefaultLayout::Grid => len.saturating_sub(1),
|
|
||||||
DefaultLayout::UltrawideVerticalStack => match len {
|
|
||||||
2 => 0,
|
|
||||||
_ => len.saturating_sub(1),
|
|
||||||
},
|
|
||||||
DefaultLayout::RightMainVerticalStack => 0,
|
|
||||||
DefaultLayout::Scrolling => len.saturating_sub(1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
|
|
||||||
pub fn resize(
|
|
||||||
&self,
|
|
||||||
unaltered: &Rect,
|
|
||||||
resize: &Option<Rect>,
|
|
||||||
edge: OperationDirection,
|
|
||||||
sizing: Sizing,
|
|
||||||
delta: i32,
|
|
||||||
) -> Option<Rect> {
|
|
||||||
if !matches!(
|
|
||||||
self,
|
|
||||||
Self::BSP
|
|
||||||
| Self::Columns
|
|
||||||
| Self::Rows
|
|
||||||
| Self::VerticalStack
|
|
||||||
| Self::RightMainVerticalStack
|
|
||||||
| Self::HorizontalStack
|
|
||||||
| Self::UltrawideVerticalStack
|
|
||||||
| Self::Scrolling
|
|
||||||
) {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut r = resize.unwrap_or_default();
|
|
||||||
|
|
||||||
let resize_delta = delta;
|
|
||||||
|
|
||||||
match edge {
|
|
||||||
OperationDirection::Left => match sizing {
|
|
||||||
Sizing::Increase => {
|
|
||||||
// Some final checks to make sure the user can't infinitely resize to
|
|
||||||
// the point of pushing other windows out of bounds
|
|
||||||
|
|
||||||
// Note: These checks cannot take into account the changes made to the
|
|
||||||
// edges of adjacent windows at operation time, so it is still possible
|
|
||||||
// to push windows out of bounds by maxing out an Increase Left on a
|
|
||||||
// Window with index 1, and then maxing out a Decrease Right on a Window
|
|
||||||
// with index 0. I don't think it's worth trying to defensively program
|
|
||||||
// against this; if people end up in this situation they are better off
|
|
||||||
// just hitting the retile command
|
|
||||||
let diff = ((r.left + -resize_delta) as f32).abs();
|
|
||||||
if diff < unaltered.right as f32 {
|
|
||||||
r.left += -resize_delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sizing::Decrease => {
|
|
||||||
let diff = ((r.left - -resize_delta) as f32).abs();
|
|
||||||
if diff < unaltered.right as f32 {
|
|
||||||
r.left -= -resize_delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
OperationDirection::Up => match sizing {
|
|
||||||
Sizing::Increase => {
|
|
||||||
let diff = ((r.top + resize_delta) as f32).abs();
|
|
||||||
if diff < unaltered.bottom as f32 {
|
|
||||||
r.top += -resize_delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sizing::Decrease => {
|
|
||||||
let diff = ((r.top - resize_delta) as f32).abs();
|
|
||||||
if diff < unaltered.bottom as f32 {
|
|
||||||
r.top -= -resize_delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
OperationDirection::Right => match sizing {
|
|
||||||
Sizing::Increase => {
|
|
||||||
let diff = ((r.right + resize_delta) as f32).abs();
|
|
||||||
if diff < unaltered.right as f32 {
|
|
||||||
r.right += resize_delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sizing::Decrease => {
|
|
||||||
let diff = ((r.right - resize_delta) as f32).abs();
|
|
||||||
if diff < unaltered.right as f32 {
|
|
||||||
r.right -= resize_delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
OperationDirection::Down => match sizing {
|
|
||||||
Sizing::Increase => {
|
|
||||||
let diff = ((r.bottom + resize_delta) as f32).abs();
|
|
||||||
if diff < unaltered.bottom as f32 {
|
|
||||||
r.bottom += resize_delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sizing::Decrease => {
|
|
||||||
let diff = ((r.bottom - resize_delta) as f32).abs();
|
|
||||||
if diff < unaltered.bottom as f32 {
|
|
||||||
r.bottom -= resize_delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if r.eq(&Rect::default()) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Option::from(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const fn cycle_next(self) -> Self {
|
|
||||||
match self {
|
|
||||||
Self::BSP => Self::Columns,
|
|
||||||
Self::Columns => Self::Rows,
|
|
||||||
Self::Rows => Self::VerticalStack,
|
|
||||||
Self::VerticalStack => Self::HorizontalStack,
|
|
||||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
|
||||||
Self::UltrawideVerticalStack => Self::Grid,
|
|
||||||
Self::Grid => Self::RightMainVerticalStack,
|
|
||||||
Self::RightMainVerticalStack => Self::Scrolling,
|
|
||||||
Self::Scrolling => Self::BSP,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const fn cycle_previous(self) -> Self {
|
|
||||||
match self {
|
|
||||||
Self::Scrolling => Self::RightMainVerticalStack,
|
|
||||||
Self::RightMainVerticalStack => Self::Grid,
|
|
||||||
Self::Grid => Self::UltrawideVerticalStack,
|
|
||||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
|
||||||
Self::HorizontalStack => Self::VerticalStack,
|
|
||||||
Self::VerticalStack => Self::Rows,
|
|
||||||
Self::Rows => Self::Columns,
|
|
||||||
Self::Columns => Self::BSP,
|
|
||||||
Self::BSP => Self::RightMainVerticalStack,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+86
-59
@@ -1,50 +1,58 @@
|
|||||||
#![warn(clippy::all)]
|
#![warn(clippy::all)]
|
||||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||||
|
#![allow(deprecated)] // allow deprecated variants like HidingBehaviour::Hide to be used in derive macros
|
||||||
|
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use color_eyre::Result;
|
use color_eyre::eyre;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
|
|
||||||
use crate::animation::prefix::AnimationPrefix;
|
|
||||||
use crate::KomorebiTheme;
|
use crate::KomorebiTheme;
|
||||||
|
use crate::animation::prefix::AnimationPrefix;
|
||||||
|
use crate::state::State;
|
||||||
|
|
||||||
|
// Re-export everything from komorebi-layouts
|
||||||
|
pub use komorebi_layouts::Arrangement;
|
||||||
|
pub use komorebi_layouts::Axis;
|
||||||
|
pub use komorebi_layouts::Column;
|
||||||
|
pub use komorebi_layouts::ColumnSplit;
|
||||||
|
pub use komorebi_layouts::ColumnSplitWithCapacity;
|
||||||
|
pub use komorebi_layouts::ColumnWidth;
|
||||||
|
pub use komorebi_layouts::CustomLayout;
|
||||||
|
pub use komorebi_layouts::CycleDirection;
|
||||||
|
pub use komorebi_layouts::DEFAULT_RATIO;
|
||||||
|
pub use komorebi_layouts::DEFAULT_SECONDARY_RATIO;
|
||||||
|
pub use komorebi_layouts::DefaultLayout;
|
||||||
|
pub use komorebi_layouts::Direction;
|
||||||
|
pub use komorebi_layouts::GridLayoutOptions;
|
||||||
|
pub use komorebi_layouts::Layout;
|
||||||
|
pub use komorebi_layouts::LayoutOptions;
|
||||||
|
pub use komorebi_layouts::MAX_RATIO;
|
||||||
|
pub use komorebi_layouts::MAX_RATIOS;
|
||||||
|
pub use komorebi_layouts::MIN_RATIO;
|
||||||
|
pub use komorebi_layouts::OperationDirection;
|
||||||
|
pub use komorebi_layouts::Rect;
|
||||||
|
pub use komorebi_layouts::ScrollingLayoutOptions;
|
||||||
|
pub use komorebi_layouts::Sizing;
|
||||||
|
pub use komorebi_layouts::validate_ratios;
|
||||||
|
|
||||||
|
// Local modules and exports
|
||||||
pub use animation::AnimationStyle;
|
pub use animation::AnimationStyle;
|
||||||
pub use arrangement::Arrangement;
|
|
||||||
pub use arrangement::Axis;
|
|
||||||
pub use custom_layout::Column;
|
|
||||||
pub use custom_layout::ColumnSplit;
|
|
||||||
pub use custom_layout::ColumnSplitWithCapacity;
|
|
||||||
pub use custom_layout::ColumnWidth;
|
|
||||||
pub use custom_layout::CustomLayout;
|
|
||||||
pub use cycle_direction::CycleDirection;
|
|
||||||
pub use default_layout::DefaultLayout;
|
|
||||||
pub use direction::Direction;
|
|
||||||
pub use layout::Layout;
|
|
||||||
pub use operation_direction::OperationDirection;
|
|
||||||
pub use pathext::replace_env_in_path;
|
|
||||||
pub use pathext::resolve_option_hashmap_usize_path;
|
|
||||||
pub use pathext::PathExt;
|
pub use pathext::PathExt;
|
||||||
pub use pathext::ResolvedPathBuf;
|
pub use pathext::ResolvedPathBuf;
|
||||||
pub use rect::Rect;
|
pub use pathext::replace_env_in_path;
|
||||||
|
pub use pathext::resolve_option_hashmap_usize_path;
|
||||||
|
|
||||||
pub mod animation;
|
pub mod animation;
|
||||||
pub mod arrangement;
|
|
||||||
pub mod asc;
|
pub mod asc;
|
||||||
pub mod config_generation;
|
pub mod config_generation;
|
||||||
pub mod custom_layout;
|
|
||||||
pub mod cycle_direction;
|
|
||||||
pub mod default_layout;
|
|
||||||
pub mod direction;
|
|
||||||
pub mod layout;
|
|
||||||
pub mod operation_direction;
|
|
||||||
pub mod pathext;
|
pub mod pathext;
|
||||||
pub mod rect;
|
|
||||||
|
|
||||||
// serde_as must be before derive
|
// serde_as must be before derive
|
||||||
#[serde_with::serde_as]
|
#[serde_with::serde_as]
|
||||||
@@ -55,6 +63,8 @@ pub enum SocketMessage {
|
|||||||
// Window / Container Commands
|
// Window / Container Commands
|
||||||
FocusWindow(OperationDirection),
|
FocusWindow(OperationDirection),
|
||||||
MoveWindow(OperationDirection),
|
MoveWindow(OperationDirection),
|
||||||
|
PreselectDirection(OperationDirection),
|
||||||
|
CancelPreselect,
|
||||||
CycleFocusWindow(CycleDirection),
|
CycleFocusWindow(CycleDirection),
|
||||||
CycleMoveWindow(CycleDirection),
|
CycleMoveWindow(CycleDirection),
|
||||||
StackWindow(OperationDirection),
|
StackWindow(OperationDirection),
|
||||||
@@ -87,6 +97,7 @@ pub enum SocketMessage {
|
|||||||
Close,
|
Close,
|
||||||
Minimize,
|
Minimize,
|
||||||
Promote,
|
Promote,
|
||||||
|
PromoteSwap,
|
||||||
PromoteFocus,
|
PromoteFocus,
|
||||||
PromoteWindow(OperationDirection),
|
PromoteWindow(OperationDirection),
|
||||||
EagerFocus(String),
|
EagerFocus(String),
|
||||||
@@ -109,6 +120,7 @@ pub enum SocketMessage {
|
|||||||
AdjustWorkspacePadding(Sizing, i32),
|
AdjustWorkspacePadding(Sizing, i32),
|
||||||
ChangeLayout(DefaultLayout),
|
ChangeLayout(DefaultLayout),
|
||||||
CycleLayout(CycleDirection),
|
CycleLayout(CycleDirection),
|
||||||
|
LayoutRatios(Option<Vec<f32>>, Option<Vec<f32>>),
|
||||||
ScrollingLayoutColumns(NonZeroUsize),
|
ScrollingLayoutColumns(NonZeroUsize),
|
||||||
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||||
FlipLayout(Axis),
|
FlipLayout(Axis),
|
||||||
@@ -245,10 +257,12 @@ pub enum SocketMessage {
|
|||||||
StaticConfigSchema,
|
StaticConfigSchema,
|
||||||
GenerateStaticConfig,
|
GenerateStaticConfig,
|
||||||
DebugWindow(isize),
|
DebugWindow(isize),
|
||||||
|
// low level commands
|
||||||
|
ApplyState(State),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SocketMessage {
|
impl SocketMessage {
|
||||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
pub fn as_bytes(&self) -> eyre::Result<Vec<u8>> {
|
||||||
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
|
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,7 +270,7 @@ impl SocketMessage {
|
|||||||
impl FromStr for SocketMessage {
|
impl FromStr for SocketMessage {
|
||||||
type Err = serde_json::Error;
|
type Err = serde_json::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> eyre::Result<Self, Self::Err> {
|
||||||
serde_json::from_str(s)
|
serde_json::from_str(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,17 +284,24 @@ pub struct SubscribeOptions {
|
|||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Stackbar mode
|
||||||
pub enum StackbarMode {
|
pub enum StackbarMode {
|
||||||
|
/// Always show
|
||||||
Always,
|
Always,
|
||||||
|
/// Never show
|
||||||
Never,
|
Never,
|
||||||
|
/// Show on stack
|
||||||
OnStack,
|
OnStack,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize)]
|
#[derive(Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Starbar label
|
||||||
pub enum StackbarLabel {
|
pub enum StackbarLabel {
|
||||||
#[default]
|
#[default]
|
||||||
|
/// Process name
|
||||||
Process,
|
Process,
|
||||||
|
/// Window title
|
||||||
Title,
|
Title,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +309,7 @@ pub enum StackbarLabel {
|
|||||||
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
|
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Border style
|
||||||
pub enum BorderStyle {
|
pub enum BorderStyle {
|
||||||
#[default]
|
#[default]
|
||||||
/// Use the system border style
|
/// Use the system border style
|
||||||
@@ -302,6 +324,7 @@ pub enum BorderStyle {
|
|||||||
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
|
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Border style
|
||||||
pub enum BorderImplementation {
|
pub enum BorderImplementation {
|
||||||
#[default]
|
#[default]
|
||||||
/// Use the adjustable komorebi border implementation
|
/// Use the adjustable komorebi border implementation
|
||||||
@@ -325,13 +348,20 @@ pub enum BorderImplementation {
|
|||||||
Hash,
|
Hash,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Window kind
|
||||||
pub enum WindowKind {
|
pub enum WindowKind {
|
||||||
|
/// Single window
|
||||||
Single,
|
Single,
|
||||||
|
/// Stack container
|
||||||
Stack,
|
Stack,
|
||||||
|
/// Monocle container
|
||||||
Monocle,
|
Monocle,
|
||||||
#[default]
|
#[default]
|
||||||
|
/// Unfocused window
|
||||||
Unfocused,
|
Unfocused,
|
||||||
|
/// Unfocused locked container
|
||||||
UnfocusedLocked,
|
UnfocusedLocked,
|
||||||
|
/// Floating window
|
||||||
Floating,
|
Floating,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,30 +382,37 @@ pub enum StateQuery {
|
|||||||
Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Application identifier
|
||||||
pub enum ApplicationIdentifier {
|
pub enum ApplicationIdentifier {
|
||||||
|
/// Executable name
|
||||||
#[serde(alias = "exe")]
|
#[serde(alias = "exe")]
|
||||||
Exe,
|
Exe,
|
||||||
|
/// Class
|
||||||
#[serde(alias = "class")]
|
#[serde(alias = "class")]
|
||||||
Class,
|
Class,
|
||||||
#[serde(alias = "title")]
|
#[serde(alias = "title")]
|
||||||
|
/// Window title
|
||||||
Title,
|
Title,
|
||||||
|
/// Executable path
|
||||||
#[serde(alias = "path")]
|
#[serde(alias = "path")]
|
||||||
Path,
|
Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Focus follows mouse implementation
|
||||||
pub enum FocusFollowsMouseImplementation {
|
pub enum FocusFollowsMouseImplementation {
|
||||||
/// A custom FFM implementation (slightly more CPU-intensive)
|
/// Custom FFM implementation (slightly more CPU-intensive)
|
||||||
Komorebi,
|
Komorebi,
|
||||||
/// The native (legacy) Windows FFM implementation
|
/// Native (legacy) Windows FFM implementation
|
||||||
Windows,
|
Windows,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Window management behaviour
|
||||||
pub struct WindowManagementBehaviour {
|
pub struct WindowManagementBehaviour {
|
||||||
/// The current WindowContainerBehaviour to be used
|
/// The current [`WindowContainerBehaviour`] to be used
|
||||||
pub current_behaviour: WindowContainerBehaviour,
|
pub current_behaviour: WindowContainerBehaviour,
|
||||||
/// Override of `current_behaviour` to open new windows as floating windows
|
/// Override of `current_behaviour` to open new windows as floating windows
|
||||||
/// that can be later toggled to tiled, when false it will default to
|
/// that can be later toggled to tiled, when false it will default to
|
||||||
@@ -394,7 +431,7 @@ pub struct WindowManagementBehaviour {
|
|||||||
pub floating_layer_placement: Placement,
|
pub floating_layer_placement: Placement,
|
||||||
/// The `Placement` to be used when spawning a window with float override active
|
/// The `Placement` to be used when spawning a window with float override active
|
||||||
pub float_override_placement: Placement,
|
pub float_override_placement: Placement,
|
||||||
/// The `Placement` to be used when spawning a window that matches a 'floating_applications' rule
|
/// The `Placement` to be used when spawning a window that matches a `floating_applications` rule
|
||||||
pub float_rule_placement: Placement,
|
pub float_rule_placement: Placement,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,6 +439,7 @@ pub struct WindowManagementBehaviour {
|
|||||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Window container behaviour when a new window is opened
|
||||||
pub enum WindowContainerBehaviour {
|
pub enum WindowContainerBehaviour {
|
||||||
/// Create a new container for each new window
|
/// Create a new container for each new window
|
||||||
#[default]
|
#[default]
|
||||||
@@ -414,6 +452,7 @@ pub enum WindowContainerBehaviour {
|
|||||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Floating layer behaviour when a new window is opened
|
||||||
pub enum FloatingLayerBehaviour {
|
pub enum FloatingLayerBehaviour {
|
||||||
/// Tile new windows (unless they match a float rule or float override is active)
|
/// Tile new windows (unless they match a float rule or float override is active)
|
||||||
#[default]
|
#[default]
|
||||||
@@ -426,6 +465,7 @@ pub enum FloatingLayerBehaviour {
|
|||||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Placement behaviour for floating windows
|
||||||
pub enum Placement {
|
pub enum Placement {
|
||||||
/// Does not change the size or position of the window
|
/// Does not change the size or position of the window
|
||||||
#[default]
|
#[default]
|
||||||
@@ -465,6 +505,7 @@ impl Placement {
|
|||||||
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Move behaviour when the operation works across a monitor boundary
|
||||||
pub enum MoveBehaviour {
|
pub enum MoveBehaviour {
|
||||||
/// Swap the window container with the window container at the edge of the adjacent monitor
|
/// Swap the window container with the window container at the edge of the adjacent monitor
|
||||||
#[default]
|
#[default]
|
||||||
@@ -479,6 +520,7 @@ pub enum MoveBehaviour {
|
|||||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Behaviour when an action would cross a monitor boundary
|
||||||
pub enum CrossBoundaryBehaviour {
|
pub enum CrossBoundaryBehaviour {
|
||||||
/// Attempt to perform actions across a workspace boundary
|
/// Attempt to perform actions across a workspace boundary
|
||||||
Workspace,
|
Workspace,
|
||||||
@@ -489,10 +531,12 @@ pub enum CrossBoundaryBehaviour {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Window hiding behaviour
|
||||||
pub enum HidingBehaviour {
|
pub enum HidingBehaviour {
|
||||||
/// END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
/// END OF LIFE FEATURE: Use the `SW_HIDE` flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||||
|
#[deprecated(note = "End of life feature")]
|
||||||
Hide,
|
Hide,
|
||||||
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
/// Use the `SW_MINIMIZE` flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||||
Minimize,
|
Minimize,
|
||||||
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||||
Cloak,
|
Cloak,
|
||||||
@@ -502,44 +546,25 @@ pub enum HidingBehaviour {
|
|||||||
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Operation behaviour for temporarily unmanaged and floating windows
|
||||||
pub enum OperationBehaviour {
|
pub enum OperationBehaviour {
|
||||||
/// Process komorebic commands on temporarily unmanaged/floated windows
|
/// Process commands on temporarily unmanaged/floated windows
|
||||||
#[default]
|
#[default]
|
||||||
Op,
|
Op,
|
||||||
/// Ignore komorebic commands on temporarily unmanaged/floated windows
|
/// Ignore commands on temporarily unmanaged/floated windows
|
||||||
NoOp,
|
NoOp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
pub enum Sizing {
|
|
||||||
Increase,
|
|
||||||
Decrease,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sizing {
|
|
||||||
#[must_use]
|
|
||||||
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
|
|
||||||
match self {
|
|
||||||
Self::Increase => value + adjustment,
|
|
||||||
Self::Decrease => {
|
|
||||||
if value > 0 && value - adjustment >= 0 {
|
|
||||||
value - adjustment
|
|
||||||
} else {
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
/// Window handling behaviour
|
||||||
pub enum WindowHandlingBehaviour {
|
pub enum WindowHandlingBehaviour {
|
||||||
#[default]
|
#[default]
|
||||||
|
/// Synchronous
|
||||||
Sync,
|
Sync,
|
||||||
|
/// Asynchronous
|
||||||
Async,
|
Async,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,7 +575,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn deserializes() {
|
fn deserializes() {
|
||||||
// Set a variable for testing
|
// Set a variable for testing
|
||||||
std::env::set_var("VAR", "VALUE");
|
unsafe {
|
||||||
|
std::env::set_var("VAR", "VALUE");
|
||||||
|
}
|
||||||
|
|
||||||
let json = r#"{"type":"WorkspaceLayoutCustomRule","content":[0,0,0,"/path/%VAR%/d"]}"#;
|
let json = r#"{"type":"WorkspaceLayoutCustomRule","content":[0,0,0,"/path/%VAR%/d"]}"#;
|
||||||
let message: SocketMessage = serde_json::from_str(json).unwrap();
|
let message: SocketMessage = serde_json::from_str(json).unwrap();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::Component;
|
use std::path::Component;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -58,7 +57,7 @@ impl<P: AsRef<Path>> PathExt for P {
|
|||||||
// if component is a variable, get the value from the environment
|
// if component is a variable, get the value from the environment
|
||||||
if let Some(var) = var {
|
if let Some(var) = var {
|
||||||
let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
|
let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
|
||||||
if let Some(value) = env::var_os(var) {
|
if let Some(value) = std::env::var_os(var) {
|
||||||
out.push(value);
|
out.push(value);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -122,13 +121,16 @@ impl<'de> serde_with::DeserializeAs<'de, PathBuf> for ResolvedPathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "schemars")]
|
#[cfg(feature = "schemars")]
|
||||||
impl serde_with::schemars_0_8::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
|
impl serde_with::schemars_1::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
|
||||||
fn schema_name() -> String {
|
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||||
"PathBuf".to_owned()
|
std::borrow::Cow::Borrowed("PathBuf")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||||
<PathBuf as schemars::JsonSchema>::json_schema(gen)
|
schemars::json_schema!({
|
||||||
|
"type": "string",
|
||||||
|
"description": "A file system path. Environment variables like %VAR%, $Env:VAR, or $VAR are automatically resolved."
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +167,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn resolves_env_vars() {
|
fn resolves_env_vars() {
|
||||||
// Set a variable for testing
|
// Set a variable for testing
|
||||||
std::env::set_var("VAR", "VALUE");
|
unsafe {
|
||||||
|
std::env::set_var("VAR", "VALUE");
|
||||||
|
}
|
||||||
|
|
||||||
// %VAR% format
|
// %VAR% format
|
||||||
assert_eq!(resolve("/path/%VAR%/d"), expected("/path/VALUE/d"));
|
assert_eq!(resolve("/path/%VAR%/d"), expected("/path/VALUE/d"));
|
||||||
@@ -183,7 +187,9 @@ mod tests {
|
|||||||
assert_eq!(resolve("/path/$ASD/to/d"), expected("/path/$ASD/to/d"));
|
assert_eq!(resolve("/path/$ASD/to/d"), expected("/path/$ASD/to/d"));
|
||||||
|
|
||||||
// Set a $env:USERPROFILE variable for testing
|
// Set a $env:USERPROFILE variable for testing
|
||||||
std::env::set_var("USERPROFILE", "C:\\Users\\user");
|
unsafe {
|
||||||
|
std::env::set_var("USERPROFILE", "C:\\Users\\user");
|
||||||
|
}
|
||||||
|
|
||||||
// ~ and $HOME should be replaced with $Env:USERPROFILE
|
// ~ and $HOME should be replaced with $Env:USERPROFILE
|
||||||
assert_eq!(resolve("~"), expected("C:\\Users\\user"));
|
assert_eq!(resolve("~"), expected("C:\\Users\\user"));
|
||||||
|
|||||||
@@ -44,13 +44,15 @@ pub fn send_notification(hwnd: isize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||||
std::thread::spawn(move || loop {
|
std::thread::spawn(move || {
|
||||||
match handle_notifications(wm.clone()) {
|
loop {
|
||||||
Ok(()) => {
|
match handle_notifications(wm.clone()) {
|
||||||
tracing::warn!("restarting finished thread");
|
Ok(()) => {
|
||||||
}
|
tracing::warn!("restarting finished thread");
|
||||||
Err(error) => {
|
}
|
||||||
tracing::warn!("restarting failed thread: {}", error);
|
Err(error) => {
|
||||||
|
tracing::warn!("restarting failed thread: {}", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+31
-9
@@ -16,7 +16,9 @@ pub mod process_event;
|
|||||||
pub mod process_movement;
|
pub mod process_movement;
|
||||||
pub mod reaper;
|
pub mod reaper;
|
||||||
pub mod set_window_position;
|
pub mod set_window_position;
|
||||||
|
pub mod splash;
|
||||||
pub mod stackbar_manager;
|
pub mod stackbar_manager;
|
||||||
|
pub mod state;
|
||||||
pub mod static_config;
|
pub mod static_config;
|
||||||
pub mod styles;
|
pub mod styles;
|
||||||
pub mod theme_manager;
|
pub mod theme_manager;
|
||||||
@@ -39,12 +41,12 @@ use std::io::Write;
|
|||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::atomic::AtomicI32;
|
use std::sync::atomic::AtomicI32;
|
||||||
use std::sync::atomic::AtomicU32;
|
use std::sync::atomic::AtomicU32;
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub use core::*;
|
pub use core::*;
|
||||||
pub use komorebi_themes::colour::*;
|
pub use komorebi_themes::colour::*;
|
||||||
@@ -62,7 +64,7 @@ use crate::core::config_generation::IdWithIdentifier;
|
|||||||
use crate::core::config_generation::MatchingRule;
|
use crate::core::config_generation::MatchingRule;
|
||||||
use crate::core::config_generation::MatchingStrategy;
|
use crate::core::config_generation::MatchingStrategy;
|
||||||
use crate::core::config_generation::WorkspaceMatchingRule;
|
use crate::core::config_generation::WorkspaceMatchingRule;
|
||||||
use color_eyre::Result;
|
use color_eyre::eyre;
|
||||||
use crossbeam_utils::atomic::AtomicCell;
|
use crossbeam_utils::atomic::AtomicCell;
|
||||||
use os_info::Version;
|
use os_info::Version;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@@ -70,10 +72,11 @@ use parking_lot::RwLock;
|
|||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use state::State;
|
||||||
use uds_windows::UnixStream;
|
use uds_windows::UnixStream;
|
||||||
use which::which;
|
use which::which;
|
||||||
use winreg::enums::HKEY_CURRENT_USER;
|
|
||||||
use winreg::RegKey;
|
use winreg::RegKey;
|
||||||
|
use winreg::enums::HKEY_CURRENT_USER;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||||
@@ -211,11 +214,10 @@ lazy_static! {
|
|||||||
pub static ref AHK_EXE: String = {
|
pub static ref AHK_EXE: String = {
|
||||||
let mut ahk: String = String::from("autohotkey.exe");
|
let mut ahk: String = String::from("autohotkey.exe");
|
||||||
|
|
||||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
|
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE")
|
||||||
if which(&komorebi_ahk_exe).is_ok() {
|
&& which(&komorebi_ahk_exe).is_ok() {
|
||||||
ahk = komorebi_ahk_exe;
|
ahk = komorebi_ahk_exe;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ahk
|
ahk
|
||||||
};
|
};
|
||||||
@@ -240,7 +242,9 @@ lazy_static! {
|
|||||||
|
|
||||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||||
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
|
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||||
|
pub static DEFAULT_RESIZE_DELTA: i32 = 50;
|
||||||
|
|
||||||
|
pub static DEFAULT_MOUSE_FOLLOWS_FOCUS: bool = true;
|
||||||
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
|
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
|
||||||
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||||
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
|
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
|
||||||
@@ -254,6 +258,21 @@ pub static WINDOW_HANDLING_BEHAVIOUR: AtomicCell<WindowHandlingBehaviour> =
|
|||||||
|
|
||||||
shadow_rs::shadow!(build);
|
shadow_rs::shadow!(build);
|
||||||
|
|
||||||
|
pub const PUBLIC_KEY: [u8; 32] = [
|
||||||
|
0x5a, 0x69, 0x4a, 0xe1, 0x3c, 0x4b, 0xc8, 0x4e, 0xc3, 0x79, 0x0f, 0xab, 0x27, 0x6b, 0x7e, 0xdd,
|
||||||
|
0x6b, 0x39, 0x6f, 0xa2, 0xc3, 0x9f, 0x3d, 0x48, 0xf2, 0x72, 0x56, 0x41, 0x1b, 0xc8, 0x08, 0xdb,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub struct License {
|
||||||
|
#[serde(rename = "hasValidSubscription")]
|
||||||
|
pub has_valid_subscription: bool,
|
||||||
|
pub timestamp: i64,
|
||||||
|
#[serde(rename = "currentEndPeriod")]
|
||||||
|
pub current_end_period: Option<i64>,
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// A trait for types that can be marked as locked or unlocked.
|
/// A trait for types that can be marked as locked or unlocked.
|
||||||
pub trait Lockable {
|
pub trait Lockable {
|
||||||
/// Returns `true` if the item is locked.
|
/// Returns `true` if the item is locked.
|
||||||
@@ -303,7 +322,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
|||||||
// the latter case, if the user desires this validation after initiating the task view, komorebi
|
// 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
|
// 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
|
// the value of CurrentVirtualDesktop and validate against it accordingly
|
||||||
current
|
current.map(|current| current.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
@@ -330,7 +349,10 @@ pub struct Notification {
|
|||||||
pub state: State,
|
pub state: State,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
|
pub fn notify_subscribers(
|
||||||
|
notification: Notification,
|
||||||
|
state_has_been_modified: bool,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
let is_override_event = matches!(
|
let is_override_event = matches!(
|
||||||
notification.event,
|
notification.event,
|
||||||
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
|
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
|
||||||
@@ -411,7 +433,7 @@ pub fn notify_subscribers(notification: Notification, state_has_been_modified: b
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_configuration() -> Result<()> {
|
pub fn load_configuration() -> eyre::Result<()> {
|
||||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||||
|
|
||||||
|
|||||||
+53
-47
@@ -10,31 +10,36 @@
|
|||||||
use std::env::temp_dir;
|
use std::env::temp_dir;
|
||||||
use std::net::Shutdown;
|
use std::net::Shutdown;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
#[cfg(feature = "deadlock_detection")]
|
#[cfg(feature = "deadlock_detection")]
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use color_eyre::eyre::anyhow;
|
use color_eyre::eyre;
|
||||||
use color_eyre::Result;
|
use color_eyre::eyre::bail;
|
||||||
use crossbeam_utils::Backoff;
|
use crossbeam_utils::Backoff;
|
||||||
use komorebi::animation::AnimationEngine;
|
|
||||||
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
|
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
|
||||||
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||||
|
use komorebi::animation::AnimationEngine;
|
||||||
use komorebi::replace_env_in_path;
|
use komorebi::replace_env_in_path;
|
||||||
|
use parking_lot::Mutex;
|
||||||
#[cfg(feature = "deadlock_detection")]
|
#[cfg(feature = "deadlock_detection")]
|
||||||
use parking_lot::deadlock;
|
use parking_lot::deadlock;
|
||||||
use parking_lot::Mutex;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sysinfo::Process;
|
use sysinfo::Process;
|
||||||
use sysinfo::ProcessesToUpdate;
|
use sysinfo::ProcessesToUpdate;
|
||||||
use tracing_appender::non_blocking::WorkerGuard;
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use uds_windows::UnixStream;
|
use uds_windows::UnixStream;
|
||||||
|
|
||||||
|
use komorebi::CUSTOM_FFM;
|
||||||
|
use komorebi::DATA_DIR;
|
||||||
|
use komorebi::HOME_DIR;
|
||||||
|
use komorebi::INITIAL_CONFIGURATION_LOADED;
|
||||||
|
use komorebi::SESSION_ID;
|
||||||
use komorebi::border_manager;
|
use komorebi::border_manager;
|
||||||
use komorebi::focus_manager;
|
use komorebi::focus_manager;
|
||||||
use komorebi::load_configuration;
|
use komorebi::load_configuration;
|
||||||
@@ -45,37 +50,36 @@ use komorebi::process_event::listen_for_events;
|
|||||||
use komorebi::process_movement::listen_for_movements;
|
use komorebi::process_movement::listen_for_movements;
|
||||||
use komorebi::reaper;
|
use komorebi::reaper;
|
||||||
use komorebi::stackbar_manager;
|
use komorebi::stackbar_manager;
|
||||||
|
use komorebi::state::State;
|
||||||
use komorebi::static_config::StaticConfig;
|
use komorebi::static_config::StaticConfig;
|
||||||
use komorebi::theme_manager;
|
use komorebi::theme_manager;
|
||||||
use komorebi::transparency_manager;
|
use komorebi::transparency_manager;
|
||||||
use komorebi::window_manager::State;
|
|
||||||
use komorebi::window_manager::WindowManager;
|
use komorebi::window_manager::WindowManager;
|
||||||
use komorebi::windows_api::WindowsApi;
|
use komorebi::windows_api::WindowsApi;
|
||||||
use komorebi::winevent_listener;
|
use komorebi::winevent_listener;
|
||||||
use komorebi::CUSTOM_FFM;
|
|
||||||
use komorebi::DATA_DIR;
|
|
||||||
use komorebi::HOME_DIR;
|
|
||||||
use komorebi::INITIAL_CONFIGURATION_LOADED;
|
|
||||||
use komorebi::SESSION_ID;
|
|
||||||
|
|
||||||
fn setup(log_level: LogLevel) -> Result<(WorkerGuard, WorkerGuard)> {
|
fn setup(log_level: LogLevel) -> eyre::Result<(WorkerGuard, WorkerGuard)> {
|
||||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
unsafe {
|
||||||
|
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
std::env::set_var(
|
unsafe {
|
||||||
"RUST_LOG",
|
std::env::set_var(
|
||||||
match log_level {
|
"RUST_LOG",
|
||||||
LogLevel::Error => "error",
|
match log_level {
|
||||||
LogLevel::Warn => "warn",
|
LogLevel::Error => "error",
|
||||||
LogLevel::Info => "info",
|
LogLevel::Warn => "warn",
|
||||||
LogLevel::Debug => "debug",
|
LogLevel::Info => "info",
|
||||||
LogLevel::Trace => "trace",
|
LogLevel::Debug => "debug",
|
||||||
},
|
LogLevel::Trace => "trace",
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
|
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
|
||||||
@@ -134,20 +138,22 @@ fn setup(log_level: LogLevel) -> Result<(WorkerGuard, WorkerGuard)> {
|
|||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
fn detect_deadlocks() {
|
fn detect_deadlocks() {
|
||||||
// Create a background thread which checks for deadlocks every 10s
|
// Create a background thread which checks for deadlocks every 10s
|
||||||
std::thread::spawn(move || loop {
|
std::thread::spawn(move || {
|
||||||
tracing::info!("running deadlock detector");
|
loop {
|
||||||
std::thread::sleep(Duration::from_secs(5));
|
tracing::info!("running deadlock detector");
|
||||||
let deadlocks = deadlock::check_deadlock();
|
std::thread::sleep(Duration::from_secs(5));
|
||||||
if deadlocks.is_empty() {
|
let deadlocks = deadlock::check_deadlock();
|
||||||
continue;
|
if deadlocks.is_empty() {
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
tracing::error!("{} deadlocks detected", deadlocks.len());
|
tracing::error!("{} deadlocks detected", deadlocks.len());
|
||||||
for (i, threads) in deadlocks.iter().enumerate() {
|
for (i, threads) in deadlocks.iter().enumerate() {
|
||||||
tracing::error!("deadlock #{}", i);
|
tracing::error!("deadlock #{}", i);
|
||||||
for t in threads {
|
for t in threads {
|
||||||
tracing::error!("thread id: {:#?}", t.thread_id());
|
tracing::error!("thread id: {:#?}", t.thread_id());
|
||||||
tracing::error!("{:#?}", t.backtrace());
|
tracing::error!("{:#?}", t.backtrace());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -170,7 +176,7 @@ struct Opts {
|
|||||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||||
#[clap(short, long = "ffm")]
|
#[clap(short, long = "ffm")]
|
||||||
focus_follows_mouse: bool,
|
focus_follows_mouse: bool,
|
||||||
/// Wait for 'komorebic complete-configuration' to be sent before processing events
|
/// Wait for `komorebic complete-configuration` to be sent before processing events
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
await_configuration: bool,
|
await_configuration: bool,
|
||||||
/// Start a TCP server on the given port to allow the direct sending of SocketMessages
|
/// Start a TCP server on the given port to allow the direct sending of SocketMessages
|
||||||
@@ -190,7 +196,7 @@ struct Opts {
|
|||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
#[allow(clippy::cognitive_complexity)]
|
#[allow(clippy::cognitive_complexity)]
|
||||||
fn main() -> Result<()> {
|
fn main() -> eyre::Result<()> {
|
||||||
let opts: Opts = Opts::parse();
|
let opts: Opts = Opts::parse();
|
||||||
CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst);
|
CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst);
|
||||||
|
|
||||||
@@ -210,9 +216,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if set_foreground_window_retries == 0 {
|
if set_foreground_window_retries == 0 {
|
||||||
return Err(anyhow!(
|
bail!("failed call to AllowSetForegroundWindow after 5 retries");
|
||||||
"failed call to AllowSetForegroundWindow after 5 retries"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,15 +233,17 @@ fn main() -> Result<()> {
|
|||||||
if matched_procs.len() > 1 {
|
if matched_procs.len() > 1 {
|
||||||
let mut len = matched_procs.len();
|
let mut len = matched_procs.len();
|
||||||
for proc in matched_procs {
|
for proc in matched_procs {
|
||||||
if let Some(executable_path) = proc.exe() {
|
if let Some(executable_path) = proc.exe()
|
||||||
if executable_path.to_string_lossy().contains("shims") {
|
&& executable_path.to_string_lossy().contains("shims")
|
||||||
len -= 1;
|
{
|
||||||
}
|
len -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len > 1 {
|
if len > 1 {
|
||||||
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
|
tracing::error!(
|
||||||
|
"komorebi.exe is already running, please exit the existing process before starting a new one"
|
||||||
|
);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-72
@@ -2,13 +2,9 @@ use std::collections::HashMap;
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
use color_eyre::eyre::anyhow;
|
use color_eyre::eyre;
|
||||||
|
use color_eyre::eyre::OptionExt;
|
||||||
use color_eyre::eyre::bail;
|
use color_eyre::eyre::bail;
|
||||||
use color_eyre::Result;
|
|
||||||
use getset::CopyGetters;
|
|
||||||
use getset::Getters;
|
|
||||||
use getset::MutGetters;
|
|
||||||
use getset::Setters;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
@@ -17,58 +13,40 @@ use crate::border_manager::BORDER_OFFSET;
|
|||||||
use crate::border_manager::BORDER_WIDTH;
|
use crate::border_manager::BORDER_WIDTH;
|
||||||
use crate::core::Rect;
|
use crate::core::Rect;
|
||||||
|
|
||||||
use crate::container::Container;
|
use crate::DEFAULT_CONTAINER_PADDING;
|
||||||
use crate::ring::Ring;
|
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||||
use crate::workspace::Workspace;
|
|
||||||
use crate::workspace::WorkspaceGlobals;
|
|
||||||
use crate::workspace::WorkspaceLayer;
|
|
||||||
use crate::DefaultLayout;
|
use crate::DefaultLayout;
|
||||||
use crate::FloatingLayerBehaviour;
|
use crate::FloatingLayerBehaviour;
|
||||||
use crate::Layout;
|
use crate::Layout;
|
||||||
use crate::OperationDirection;
|
use crate::OperationDirection;
|
||||||
use crate::Wallpaper;
|
use crate::Wallpaper;
|
||||||
use crate::WindowsApi;
|
use crate::WindowsApi;
|
||||||
use crate::DEFAULT_CONTAINER_PADDING;
|
use crate::container::Container;
|
||||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
use crate::ring::Ring;
|
||||||
|
use crate::workspace::Workspace;
|
||||||
|
use crate::workspace::WorkspaceGlobals;
|
||||||
|
use crate::workspace::WorkspaceLayer;
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, PartialEq,
|
|
||||||
)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct Monitor {
|
pub struct Monitor {
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
|
||||||
pub id: isize,
|
pub id: isize,
|
||||||
#[getset(get = "pub", set = "pub")]
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[getset(get = "pub", set = "pub")]
|
|
||||||
pub device: String,
|
pub device: String,
|
||||||
#[getset(get = "pub", set = "pub")]
|
|
||||||
pub device_id: String,
|
pub device_id: String,
|
||||||
#[getset(get = "pub", set = "pub")]
|
|
||||||
pub serial_number_id: Option<String>,
|
pub serial_number_id: Option<String>,
|
||||||
#[getset(get = "pub", set = "pub")]
|
|
||||||
pub size: Rect,
|
pub size: Rect,
|
||||||
#[getset(get = "pub", set = "pub")]
|
|
||||||
pub work_area_size: Rect,
|
pub work_area_size: Rect,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
|
||||||
pub work_area_offset: Option<Rect>,
|
pub work_area_offset: Option<Rect>,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
|
||||||
pub window_based_work_area_offset: Option<Rect>,
|
pub window_based_work_area_offset: Option<Rect>,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
|
||||||
pub window_based_work_area_offset_limit: isize,
|
pub window_based_work_area_offset_limit: isize,
|
||||||
pub workspaces: Ring<Workspace>,
|
pub workspaces: Ring<Workspace>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
|
||||||
pub last_focused_workspace: Option<usize>,
|
pub last_focused_workspace: Option<usize>,
|
||||||
#[getset(get_mut = "pub")]
|
|
||||||
pub workspace_names: HashMap<usize, String>,
|
pub workspace_names: HashMap<usize, String>,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
|
||||||
pub container_padding: Option<i32>,
|
pub container_padding: Option<i32>,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
|
||||||
pub workspace_padding: Option<i32>,
|
pub workspace_padding: Option<i32>,
|
||||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
|
||||||
pub wallpaper: Option<Wallpaper>,
|
pub wallpaper: Option<Wallpaper>,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
|
||||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,23 +153,23 @@ impl Monitor {
|
|||||||
|
|
||||||
pub fn focused_workspace_name(&self) -> Option<String> {
|
pub fn focused_workspace_name(&self) -> Option<String> {
|
||||||
self.focused_workspace()
|
self.focused_workspace()
|
||||||
.map(|w| w.name().clone())
|
.map(|w| w.name.clone())
|
||||||
.unwrap_or(None)
|
.unwrap_or(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focused_workspace_layout(&self) -> Option<Layout> {
|
pub fn focused_workspace_layout(&self) -> Option<Layout> {
|
||||||
self.focused_workspace().and_then(|workspace| {
|
self.focused_workspace().and_then(|workspace| {
|
||||||
if *workspace.tile() {
|
if workspace.tile {
|
||||||
Some(workspace.layout().clone())
|
Some(workspace.layout.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> eyre::Result<()> {
|
||||||
let focused_idx = self.focused_workspace_idx();
|
let focused_idx = self.focused_workspace_idx();
|
||||||
let hmonitor = self.id();
|
let hmonitor = self.id;
|
||||||
let monitor_wp = self.wallpaper.clone();
|
let monitor_wp = self.wallpaper.clone();
|
||||||
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
|
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
|
||||||
if i == focused_idx {
|
if i == focused_idx {
|
||||||
@@ -207,10 +185,10 @@ impl Monitor {
|
|||||||
/// Updates the `globals` field of all workspaces
|
/// Updates the `globals` field of all workspaces
|
||||||
pub fn update_workspaces_globals(&mut self, offset: Option<Rect>) {
|
pub fn update_workspaces_globals(&mut self, offset: Option<Rect>) {
|
||||||
let container_padding = self
|
let container_padding = self
|
||||||
.container_padding()
|
.container_padding
|
||||||
.or(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
|
.or(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
|
||||||
let workspace_padding = self
|
let workspace_padding = self
|
||||||
.workspace_padding()
|
.workspace_padding
|
||||||
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
|
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
|
||||||
let (border_width, border_offset) = {
|
let (border_width, border_offset) = {
|
||||||
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
|
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
|
||||||
@@ -222,11 +200,11 @@ impl Monitor {
|
|||||||
(0, 0)
|
(0, 0)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let work_area = *self.work_area_size();
|
let work_area = self.work_area_size;
|
||||||
let work_area_offset = self.work_area_offset.or(offset);
|
let work_area_offset = self.work_area_offset.or(offset);
|
||||||
let window_based_work_area_offset = self.window_based_work_area_offset();
|
let window_based_work_area_offset = self.window_based_work_area_offset;
|
||||||
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit();
|
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit;
|
||||||
let floating_layer_behaviour = self.floating_layer_behaviour();
|
let floating_layer_behaviour = self.floating_layer_behaviour;
|
||||||
|
|
||||||
for workspace in self.workspaces_mut() {
|
for workspace in self.workspaces_mut() {
|
||||||
workspace.globals = WorkspaceGlobals {
|
workspace.globals = WorkspaceGlobals {
|
||||||
@@ -246,10 +224,10 @@ impl Monitor {
|
|||||||
/// Updates the `globals` field of workspace with index `workspace_idx`
|
/// Updates the `globals` field of workspace with index `workspace_idx`
|
||||||
pub fn update_workspace_globals(&mut self, workspace_idx: usize, offset: Option<Rect>) {
|
pub fn update_workspace_globals(&mut self, workspace_idx: usize, offset: Option<Rect>) {
|
||||||
let container_padding = self
|
let container_padding = self
|
||||||
.container_padding()
|
.container_padding
|
||||||
.or(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
|
.or(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
|
||||||
let workspace_padding = self
|
let workspace_padding = self
|
||||||
.workspace_padding()
|
.workspace_padding
|
||||||
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
|
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
|
||||||
let (border_width, border_offset) = {
|
let (border_width, border_offset) = {
|
||||||
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
|
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
|
||||||
@@ -261,11 +239,11 @@ impl Monitor {
|
|||||||
(0, 0)
|
(0, 0)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let work_area = *self.work_area_size();
|
let work_area = self.work_area_size;
|
||||||
let work_area_offset = self.work_area_offset.or(offset);
|
let work_area_offset = self.work_area_offset.or(offset);
|
||||||
let window_based_work_area_offset = self.window_based_work_area_offset();
|
let window_based_work_area_offset = self.window_based_work_area_offset;
|
||||||
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit();
|
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit;
|
||||||
let floating_layer_behaviour = self.floating_layer_behaviour();
|
let floating_layer_behaviour = self.floating_layer_behaviour;
|
||||||
|
|
||||||
if let Some(workspace) = self.workspaces_mut().get_mut(workspace_idx) {
|
if let Some(workspace) = self.workspaces_mut().get_mut(workspace_idx) {
|
||||||
workspace.globals = WorkspaceGlobals {
|
workspace.globals = WorkspaceGlobals {
|
||||||
@@ -286,14 +264,14 @@ impl Monitor {
|
|||||||
&mut self,
|
&mut self,
|
||||||
container: Container,
|
container: Container,
|
||||||
workspace_idx: Option<usize>,
|
workspace_idx: Option<usize>,
|
||||||
) -> Result<()> {
|
) -> eyre::Result<()> {
|
||||||
let workspace = if let Some(idx) = workspace_idx {
|
let workspace = if let Some(idx) = workspace_idx {
|
||||||
self.workspaces_mut()
|
self.workspaces_mut()
|
||||||
.get_mut(idx)
|
.get_mut(idx)
|
||||||
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
|
.ok_or_eyre(format!("there is no workspace at index {idx}"))?
|
||||||
} else {
|
} else {
|
||||||
self.focused_workspace_mut()
|
self.focused_workspace_mut()
|
||||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
.ok_or_eyre("there is no workspace")?
|
||||||
};
|
};
|
||||||
|
|
||||||
workspace.add_container_to_back(container);
|
workspace.add_container_to_back(container);
|
||||||
@@ -310,21 +288,21 @@ impl Monitor {
|
|||||||
container: Container,
|
container: Container,
|
||||||
workspace_idx: Option<usize>,
|
workspace_idx: Option<usize>,
|
||||||
direction: OperationDirection,
|
direction: OperationDirection,
|
||||||
) -> Result<()> {
|
) -> eyre::Result<()> {
|
||||||
let workspace = if let Some(idx) = workspace_idx {
|
let workspace = if let Some(idx) = workspace_idx {
|
||||||
self.workspaces_mut()
|
self.workspaces_mut()
|
||||||
.get_mut(idx)
|
.get_mut(idx)
|
||||||
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
|
.ok_or_eyre(format!("there is no workspace at index {idx}"))?
|
||||||
} else {
|
} else {
|
||||||
self.focused_workspace_mut()
|
self.focused_workspace_mut()
|
||||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
.ok_or_eyre("there is no workspace")?
|
||||||
};
|
};
|
||||||
|
|
||||||
match direction {
|
match direction {
|
||||||
OperationDirection::Left => {
|
OperationDirection::Left => {
|
||||||
// insert the container into the workspace on the monitor at the back (or rightmost position)
|
// insert the container into the workspace on the monitor at the back (or rightmost position)
|
||||||
// if we are moving across a boundary to the left (back = right side of the target)
|
// if we are moving across a boundary to the left (back = right side of the target)
|
||||||
match workspace.layout() {
|
match workspace.layout {
|
||||||
Layout::Default(layout) => match layout {
|
Layout::Default(layout) => match layout {
|
||||||
DefaultLayout::RightMainVerticalStack => {
|
DefaultLayout::RightMainVerticalStack => {
|
||||||
workspace.add_container_to_front(container);
|
workspace.add_container_to_front(container);
|
||||||
@@ -348,7 +326,7 @@ impl Monitor {
|
|||||||
OperationDirection::Right => {
|
OperationDirection::Right => {
|
||||||
// insert the container into the workspace on the monitor at the front (or leftmost position)
|
// insert the container into the workspace on the monitor at the front (or leftmost position)
|
||||||
// if we are moving across a boundary to the right (front = left side of the target)
|
// if we are moving across a boundary to the right (front = left side of the target)
|
||||||
match workspace.layout() {
|
match workspace.layout {
|
||||||
Layout::Default(layout) => {
|
Layout::Default(layout) => {
|
||||||
let target_index = layout.leftmost_index(workspace.containers().len());
|
let target_index = layout.leftmost_index(workspace.containers().len());
|
||||||
|
|
||||||
@@ -412,12 +390,12 @@ impl Monitor {
|
|||||||
target_workspace_idx: usize,
|
target_workspace_idx: usize,
|
||||||
follow: bool,
|
follow: bool,
|
||||||
direction: Option<OperationDirection>,
|
direction: Option<OperationDirection>,
|
||||||
) -> Result<()> {
|
) -> eyre::Result<()> {
|
||||||
let workspace = self
|
let workspace = self
|
||||||
.focused_workspace_mut()
|
.focused_workspace_mut()
|
||||||
.ok_or_else(|| anyhow!("there is no workspace"))?;
|
.ok_or_eyre("there is no workspace")?;
|
||||||
|
|
||||||
if workspace.maximized_window().is_some() {
|
if workspace.maximized_window.is_some() {
|
||||||
bail!("cannot move native maximized window to another monitor or workspace");
|
bail!("cannot move native maximized window to another monitor or workspace");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,12 +418,12 @@ impl Monitor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
target_workspace.floating_windows_mut().push_back(window);
|
target_workspace.floating_windows_mut().push_back(window);
|
||||||
target_workspace.set_layer(WorkspaceLayer::Floating);
|
target_workspace.layer = WorkspaceLayer::Floating;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let container = workspace
|
let container = workspace
|
||||||
.remove_focused_container()
|
.remove_focused_container()
|
||||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
.ok_or_eyre("there is no container")?;
|
||||||
|
|
||||||
let workspaces = self.workspaces_mut();
|
let workspaces = self.workspaces_mut();
|
||||||
|
|
||||||
@@ -458,7 +436,7 @@ impl Monitor {
|
|||||||
Some(workspace) => workspace,
|
Some(workspace) => workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
if target_workspace.monocle_container().is_some() {
|
if target_workspace.monocle_container.is_some() {
|
||||||
for container in target_workspace.containers_mut() {
|
for container in target_workspace.containers_mut() {
|
||||||
container.restore();
|
container.restore();
|
||||||
}
|
}
|
||||||
@@ -470,7 +448,7 @@ impl Monitor {
|
|||||||
target_workspace.reintegrate_monocle_container()?;
|
target_workspace.reintegrate_monocle_container()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
target_workspace.set_layer(WorkspaceLayer::Tiling);
|
target_workspace.layer = WorkspaceLayer::Tiling;
|
||||||
|
|
||||||
if let Some(direction) = direction {
|
if let Some(direction) = direction {
|
||||||
self.add_container_with_direction(
|
self.add_container_with_direction(
|
||||||
@@ -491,7 +469,7 @@ impl Monitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn focus_workspace(&mut self, idx: usize) -> Result<()> {
|
pub fn focus_workspace(&mut self, idx: usize) -> eyre::Result<()> {
|
||||||
tracing::info!("focusing workspace");
|
tracing::info!("focusing workspace");
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -500,7 +478,7 @@ impl Monitor {
|
|||||||
if workspaces.get(idx).is_none() {
|
if workspaces.get(idx).is_none() {
|
||||||
workspaces.resize(idx + 1, Workspace::default());
|
workspaces.resize(idx + 1, Workspace::default());
|
||||||
}
|
}
|
||||||
self.set_last_focused_workspace(Some(self.workspaces.focused_idx()));
|
self.last_focused_workspace = Some(self.workspaces.focused_idx());
|
||||||
self.workspaces.focus(idx);
|
self.workspaces.focus(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,8 +488,8 @@ impl Monitor {
|
|||||||
if name.is_some() {
|
if name.is_some() {
|
||||||
self.workspaces_mut()
|
self.workspaces_mut()
|
||||||
.get_mut(idx)
|
.get_mut(idx)
|
||||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
.ok_or_eyre("there is no workspace")?
|
||||||
.set_name(name);
|
.name = name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,9 +500,9 @@ impl Monitor {
|
|||||||
self.workspaces().len()
|
self.workspaces().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> Result<()> {
|
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> eyre::Result<()> {
|
||||||
let offset = if self.work_area_offset().is_some() {
|
let offset = if self.work_area_offset.is_some() {
|
||||||
self.work_area_offset()
|
self.work_area_offset
|
||||||
} else {
|
} else {
|
||||||
offset
|
offset
|
||||||
};
|
};
|
||||||
@@ -532,7 +510,7 @@ impl Monitor {
|
|||||||
let focused_workspace_idx = self.focused_workspace_idx();
|
let focused_workspace_idx = self.focused_workspace_idx();
|
||||||
self.update_workspace_globals(focused_workspace_idx, offset);
|
self.update_workspace_globals(focused_workspace_idx, offset);
|
||||||
self.focused_workspace_mut()
|
self.focused_workspace_mut()
|
||||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
.ok_or_eyre("there is no workspace")?
|
||||||
.update()?;
|
.update()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use windows::core::PCWSTR;
|
|
||||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_DISPLAY_ADAPTER;
|
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_DISPLAY_ADAPTER;
|
||||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_MONITOR;
|
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_MONITOR;
|
||||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_VIDEO_OUTPUT_ARRIVAL;
|
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_VIDEO_OUTPUT_ARRIVAL;
|
||||||
@@ -11,10 +10,6 @@ use windows::Win32::Foundation::LRESULT;
|
|||||||
use windows::Win32::Foundation::WPARAM;
|
use windows::Win32::Foundation::WPARAM;
|
||||||
use windows::Win32::System::Power::POWERBROADCAST_SETTING;
|
use windows::Win32::System::Power::POWERBROADCAST_SETTING;
|
||||||
use windows::Win32::System::SystemServices::GUID_LIDSWITCH_STATE_CHANGE;
|
use windows::Win32::System::SystemServices::GUID_LIDSWITCH_STATE_CHANGE;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::DBT_CONFIGCHANGED;
|
use windows::Win32::UI::WindowsAndMessaging::DBT_CONFIGCHANGED;
|
||||||
@@ -23,6 +18,9 @@ use windows::Win32::UI::WindowsAndMessaging::DBT_DEVICEREMOVECOMPLETE;
|
|||||||
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
|
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVTYP_DEVICEINTERFACE;
|
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVTYP_DEVICEINTERFACE;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::DEV_BROADCAST_DEVICEINTERFACE_W;
|
use windows::Win32::UI::WindowsAndMessaging::DEV_BROADCAST_DEVICEINTERFACE_W;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC;
|
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND;
|
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND;
|
||||||
@@ -30,6 +28,7 @@ use windows::Win32::UI::WindowsAndMessaging::PBT_APMSUSPEND;
|
|||||||
use windows::Win32::UI::WindowsAndMessaging::PBT_POWERSETTINGCHANGE;
|
use windows::Win32::UI::WindowsAndMessaging::PBT_POWERSETTINGCHANGE;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::REGISTER_NOTIFICATION_FLAGS;
|
use windows::Win32::UI::WindowsAndMessaging::REGISTER_NOTIFICATION_FLAGS;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
|
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
|
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
|
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_POWERBROADCAST;
|
use windows::Win32::UI::WindowsAndMessaging::WM_POWERBROADCAST;
|
||||||
@@ -38,10 +37,11 @@ use windows::Win32::UI::WindowsAndMessaging::WM_WTSSESSION_CHANGE;
|
|||||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK;
|
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
|
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
|
||||||
|
use windows::core::PCWSTR;
|
||||||
|
|
||||||
|
use crate::WindowsApi;
|
||||||
use crate::monitor_reconciliator;
|
use crate::monitor_reconciliator;
|
||||||
use crate::windows_api;
|
use crate::windows_api;
|
||||||
use crate::WindowsApi;
|
|
||||||
|
|
||||||
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
|
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -224,14 +224,18 @@ impl Hidden {
|
|||||||
WM_WTSSESSION_CHANGE => {
|
WM_WTSSESSION_CHANGE => {
|
||||||
match wparam.0 as u32 {
|
match wparam.0 as u32 {
|
||||||
WTS_SESSION_LOCK => {
|
WTS_SESSION_LOCK => {
|
||||||
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked");
|
tracing::debug!(
|
||||||
|
"WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked"
|
||||||
|
);
|
||||||
|
|
||||||
monitor_reconciliator::send_notification(
|
monitor_reconciliator::send_notification(
|
||||||
monitor_reconciliator::MonitorNotification::SessionLocked,
|
monitor_reconciliator::MonitorNotification::SessionLocked,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
WTS_SESSION_UNLOCK => {
|
WTS_SESSION_UNLOCK => {
|
||||||
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked");
|
tracing::debug!(
|
||||||
|
"WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked"
|
||||||
|
);
|
||||||
|
|
||||||
monitor_reconciliator::send_notification(
|
monitor_reconciliator::send_notification(
|
||||||
monitor_reconciliator::MonitorNotification::SessionUnlocked,
|
monitor_reconciliator::MonitorNotification::SessionUnlocked,
|
||||||
@@ -251,7 +255,8 @@ impl Hidden {
|
|||||||
// and resolution changes here
|
// and resolution changes here
|
||||||
WM_DISPLAYCHANGE => {
|
WM_DISPLAYCHANGE => {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed", wparam.0
|
"WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed",
|
||||||
|
wparam.0
|
||||||
);
|
);
|
||||||
|
|
||||||
monitor_reconciliator::send_notification(
|
monitor_reconciliator::send_notification(
|
||||||
@@ -265,8 +270,8 @@ impl Hidden {
|
|||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
if wparam.0 as u32 == SPI_SETWORKAREA.0 {
|
if wparam.0 as u32 == SPI_SETWORKAREA.0 {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)"
|
"WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)"
|
||||||
);
|
);
|
||||||
|
|
||||||
monitor_reconciliator::send_notification(
|
monitor_reconciliator::send_notification(
|
||||||
monitor_reconciliator::MonitorNotification::WorkAreaChanged,
|
monitor_reconciliator::MonitorNotification::WorkAreaChanged,
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
|
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||||
|
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
|
||||||
|
use crate::Notification;
|
||||||
|
use crate::NotificationEvent;
|
||||||
|
use crate::WORKSPACE_MATCHING_RULES;
|
||||||
|
use crate::WindowManager;
|
||||||
|
use crate::WindowsApi;
|
||||||
use crate::border_manager;
|
use crate::border_manager;
|
||||||
use crate::config_generation::WorkspaceMatchingRule;
|
use crate::config_generation::WorkspaceMatchingRule;
|
||||||
use crate::core::Rect;
|
use crate::core::Rect;
|
||||||
@@ -7,14 +14,7 @@ use crate::monitor;
|
|||||||
use crate::monitor::Monitor;
|
use crate::monitor::Monitor;
|
||||||
use crate::monitor_reconciliator::hidden::Hidden;
|
use crate::monitor_reconciliator::hidden::Hidden;
|
||||||
use crate::notify_subscribers;
|
use crate::notify_subscribers;
|
||||||
use crate::Notification;
|
use crate::state::State;
|
||||||
use crate::NotificationEvent;
|
|
||||||
use crate::State;
|
|
||||||
use crate::WindowManager;
|
|
||||||
use crate::WindowsApi;
|
|
||||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
|
||||||
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
|
|
||||||
use crate::WORKSPACE_MATCHING_RULES;
|
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use crossbeam_utils::atomic::AtomicConsume;
|
use crossbeam_utils::atomic::AtomicConsume;
|
||||||
@@ -22,10 +22,11 @@ use parking_lot::Mutex;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::atomic::AtomicI64;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
pub mod hidden;
|
pub mod hidden;
|
||||||
|
|
||||||
@@ -44,6 +45,10 @@ pub enum MonitorNotification {
|
|||||||
|
|
||||||
static ACTIVE: AtomicBool = AtomicBool::new(true);
|
static ACTIVE: AtomicBool = AtomicBool::new(true);
|
||||||
|
|
||||||
|
/// Timestamp (epoch millis) of the last DisplayConnectionChange notification.
|
||||||
|
/// Used to suppress OS-initiated window minimizes during transient display events.
|
||||||
|
static LAST_DISPLAY_CHANGE_TIMESTAMP: AtomicI64 = AtomicI64::new(0);
|
||||||
|
|
||||||
static CHANNEL: OnceLock<(Sender<MonitorNotification>, Receiver<MonitorNotification>)> =
|
static CHANNEL: OnceLock<(Sender<MonitorNotification>, Receiver<MonitorNotification>)> =
|
||||||
OnceLock::new();
|
OnceLock::new();
|
||||||
|
|
||||||
@@ -62,18 +67,47 @@ fn event_rx() -> Receiver<MonitorNotification> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_notification(notification: MonitorNotification) {
|
pub fn send_notification(notification: MonitorNotification) {
|
||||||
|
if matches!(
|
||||||
|
notification,
|
||||||
|
MonitorNotification::DisplayConnectionChange
|
||||||
|
| MonitorNotification::ResumingFromSuspendedState
|
||||||
|
| MonitorNotification::SessionUnlocked
|
||||||
|
) {
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as i64;
|
||||||
|
LAST_DISPLAY_CHANGE_TIMESTAMP.store(now, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
if event_tx().try_send(notification).is_err() {
|
if event_tx().try_send(notification).is_err() {
|
||||||
tracing::warn!("channel is full; dropping notification")
|
tracing::warn!("channel is full; dropping notification")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if a display connection change event was received within the
|
||||||
|
/// last `grace_period` duration. This is used by the event processor to avoid
|
||||||
|
/// treating OS-initiated minimizes (caused by transient monitor disconnects)
|
||||||
|
/// as user-initiated minimizes.
|
||||||
|
pub fn display_change_in_progress(grace_period: std::time::Duration) -> bool {
|
||||||
|
let last = LAST_DISPLAY_CHANGE_TIMESTAMP.load(Ordering::SeqCst);
|
||||||
|
if last == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as i64;
|
||||||
|
(now - last) < grace_period.as_millis() as i64
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
|
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
|
||||||
let dip = DISPLAY_INDEX_PREFERENCES.read();
|
let dip = DISPLAY_INDEX_PREFERENCES.read();
|
||||||
let mut dip_ids = dip.values();
|
let mut dip_ids = dip.values();
|
||||||
let preferred_id = if dip_ids.any(|id| id == monitor.device_id()) {
|
let preferred_id = if dip_ids.any(|id| id.eq(&monitor.device_id)) {
|
||||||
monitor.device_id().clone()
|
monitor.device_id.clone()
|
||||||
} else if dip_ids.any(|id| Some(id) == monitor.serial_number_id().as_ref()) {
|
} else if dip_ids.any(|id| Some(id) == monitor.serial_number_id.as_ref()) {
|
||||||
monitor.serial_number_id().clone().unwrap_or_default()
|
monitor.serial_number_id.clone().unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
serial_or_device_id.to_string()
|
serial_or_device_id.to_string()
|
||||||
};
|
};
|
||||||
@@ -89,7 +123,41 @@ where
|
|||||||
F: Fn() -> I + Copy,
|
F: Fn() -> I + Copy,
|
||||||
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
|
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
|
||||||
{
|
{
|
||||||
let all_displays = display_provider().flatten().collect::<Vec<_>>();
|
let mut attempts = 0;
|
||||||
|
|
||||||
|
let (displays, errors) = loop {
|
||||||
|
let (displays, errors): (Vec<_>, Vec<_>) = display_provider().partition(Result::is_ok);
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
break (displays, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
for err in &errors {
|
||||||
|
if let Err(e) = err {
|
||||||
|
tracing::warn!(
|
||||||
|
"enumerating display in reconciliator (attempt {}): {:?}",
|
||||||
|
attempts + 1,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempts < 5 {
|
||||||
|
attempts += 1;
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break (displays, errors);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !errors.is_empty() {
|
||||||
|
return Err(color_eyre::eyre::eyre!(
|
||||||
|
"could not successfully enumerate all displays"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let all_displays = displays.into_iter().map(Result::unwrap).collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut serial_id_map = HashMap::new();
|
let mut serial_id_map = HashMap::new();
|
||||||
|
|
||||||
@@ -100,12 +168,12 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
for d in &all_displays {
|
for d in &all_displays {
|
||||||
if let Some(id) = &d.serial_number_id {
|
if let Some(id) = &d.serial_number_id
|
||||||
if serial_id_map.get(id).copied().unwrap_or_default() > 1 {
|
&& serial_id_map.get(id).copied().unwrap_or_default() > 1
|
||||||
let mut dupes = DUPLICATE_MONITOR_SERIAL_IDS.write();
|
{
|
||||||
if !dupes.contains(id) {
|
let mut dupes = DUPLICATE_MONITOR_SERIAL_IDS.write();
|
||||||
(*dupes).push(id.clone());
|
if !dupes.contains(id) {
|
||||||
}
|
(*dupes).push(id.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,16 +223,18 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Re
|
|||||||
|
|
||||||
tracing::info!("created hidden window to listen for monitor-related events");
|
tracing::info!("created hidden window to listen for monitor-related events");
|
||||||
|
|
||||||
std::thread::spawn(move || loop {
|
std::thread::spawn(move || {
|
||||||
match handle_notifications(wm.clone(), win32_display_data::connected_displays_all) {
|
loop {
|
||||||
Ok(()) => {
|
match handle_notifications(wm.clone(), win32_display_data::connected_displays_all) {
|
||||||
tracing::warn!("restarting finished thread");
|
Ok(()) => {
|
||||||
}
|
tracing::warn!("restarting finished thread");
|
||||||
Err(error) => {
|
}
|
||||||
if cfg!(debug_assertions) {
|
Err(error) => {
|
||||||
tracing::error!("restarting failed thread: {:?}", error)
|
if cfg!(debug_assertions) {
|
||||||
} else {
|
tracing::error!("restarting failed thread: {:?}", error)
|
||||||
tracing::error!("restarting failed thread: {}", error)
|
} else {
|
||||||
|
tracing::error!("restarting failed thread: {}", error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,6 +271,8 @@ where
|
|||||||
border_manager::send_notification(None);
|
border_manager::send_notification(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep reference to Arc for potential re-locking
|
||||||
|
let wm_arc = Arc::clone(&wm);
|
||||||
let mut wm = wm.lock();
|
let mut wm = wm.lock();
|
||||||
|
|
||||||
let initial_state = State::from(wm.as_ref());
|
let initial_state = State::from(wm.as_ref());
|
||||||
@@ -219,27 +291,27 @@ where
|
|||||||
let mut should_update = false;
|
let mut should_update = false;
|
||||||
|
|
||||||
// Update work areas as necessary
|
// Update work areas as necessary
|
||||||
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
|
if let Ok(reference) = WindowsApi::monitor(monitor.id)
|
||||||
if reference.work_area_size() != monitor.work_area_size() {
|
&& reference.work_area_size != monitor.work_area_size
|
||||||
monitor.set_work_area_size(Rect {
|
{
|
||||||
left: reference.work_area_size().left,
|
monitor.work_area_size = Rect {
|
||||||
top: reference.work_area_size().top,
|
left: reference.work_area_size.left,
|
||||||
right: reference.work_area_size().right,
|
top: reference.work_area_size.top,
|
||||||
bottom: reference.work_area_size().bottom,
|
right: reference.work_area_size.right,
|
||||||
});
|
bottom: reference.work_area_size.bottom,
|
||||||
|
};
|
||||||
|
|
||||||
should_update = true;
|
should_update = true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if should_update {
|
if should_update {
|
||||||
tracing::info!("updated work area for {}", monitor.device_id());
|
tracing::info!("updated work area for {}", monitor.device_id);
|
||||||
monitor.update_focused_workspace(offset)?;
|
monitor.update_focused_workspace(offset)?;
|
||||||
border_manager::send_notification(None);
|
border_manager::send_notification(None);
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"work areas match, reconciliation not required for {}",
|
"work areas match, reconciliation not required for {}",
|
||||||
monitor.device_id()
|
monitor.device_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,25 +323,25 @@ where
|
|||||||
let mut should_update = false;
|
let mut should_update = false;
|
||||||
|
|
||||||
// Update sizes and work areas as necessary
|
// Update sizes and work areas as necessary
|
||||||
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
|
if let Ok(reference) = WindowsApi::monitor(monitor.id) {
|
||||||
if reference.work_area_size() != monitor.work_area_size() {
|
if reference.work_area_size != monitor.work_area_size {
|
||||||
monitor.set_work_area_size(Rect {
|
monitor.work_area_size = Rect {
|
||||||
left: reference.work_area_size().left,
|
left: reference.work_area_size.left,
|
||||||
top: reference.work_area_size().top,
|
top: reference.work_area_size.top,
|
||||||
right: reference.work_area_size().right,
|
right: reference.work_area_size.right,
|
||||||
bottom: reference.work_area_size().bottom,
|
bottom: reference.work_area_size.bottom,
|
||||||
});
|
};
|
||||||
|
|
||||||
should_update = true;
|
should_update = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if reference.size() != monitor.size() {
|
if reference.size != monitor.size {
|
||||||
monitor.set_size(Rect {
|
monitor.size = Rect {
|
||||||
left: reference.size().left,
|
left: reference.size.left,
|
||||||
top: reference.size().top,
|
top: reference.size.top,
|
||||||
right: reference.size().right,
|
right: reference.size.right,
|
||||||
bottom: reference.size().bottom,
|
bottom: reference.size.bottom,
|
||||||
});
|
};
|
||||||
|
|
||||||
should_update = true;
|
should_update = true;
|
||||||
}
|
}
|
||||||
@@ -278,7 +350,7 @@ where
|
|||||||
if should_update {
|
if should_update {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"updated monitor resolution/scaling for {}",
|
"updated monitor resolution/scaling for {}",
|
||||||
monitor.device_id()
|
monitor.device_id
|
||||||
);
|
);
|
||||||
|
|
||||||
monitor.update_focused_workspace(offset)?;
|
monitor.update_focused_workspace(offset)?;
|
||||||
@@ -286,7 +358,7 @@ where
|
|||||||
} else {
|
} else {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"resolutions match, reconciliation not required for {}",
|
"resolutions match, reconciliation not required for {}",
|
||||||
monitor.device_id()
|
monitor.device_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,21 +383,21 @@ where
|
|||||||
for monitor in wm.monitors_mut() {
|
for monitor in wm.monitors_mut() {
|
||||||
for attached in &attached_devices {
|
for attached in &attached_devices {
|
||||||
let serial_number_ids_match = if let (Some(attached_snid), Some(m_snid)) =
|
let serial_number_ids_match = if let (Some(attached_snid), Some(m_snid)) =
|
||||||
(attached.serial_number_id(), monitor.serial_number_id())
|
(&attached.serial_number_id, &monitor.serial_number_id)
|
||||||
{
|
{
|
||||||
attached_snid.eq(m_snid)
|
attached_snid.eq(m_snid)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
if serial_number_ids_match || attached.device_id().eq(monitor.device_id()) {
|
if serial_number_ids_match || attached.device_id.eq(&monitor.device_id) {
|
||||||
monitor.set_id(attached.id());
|
monitor.id = attached.id;
|
||||||
monitor.set_device(attached.device().clone());
|
monitor.device = attached.device.clone();
|
||||||
monitor.set_device_id(attached.device_id().clone());
|
monitor.device_id = attached.device_id.clone();
|
||||||
monitor.set_serial_number_id(attached.serial_number_id().clone());
|
monitor.serial_number_id = attached.serial_number_id.clone();
|
||||||
monitor.set_name(attached.name().clone());
|
monitor.name = attached.name.clone();
|
||||||
monitor.set_size(*attached.size());
|
monitor.size = attached.size;
|
||||||
monitor.set_work_area_size(*attached.work_area_size());
|
monitor.work_area_size = attached.work_area_size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,12 +416,180 @@ where
|
|||||||
continue 'receiver;
|
continue 'receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
if initial_monitor_count > attached_devices.len() {
|
// Handle potential monitor removal with verification
|
||||||
|
let attached_devices = if initial_monitor_count > attached_devices.len() {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"monitor count mismatch ({initial_monitor_count} vs {}), removing disconnected monitors",
|
"potential monitor removal detected ({initial_monitor_count} vs {}), verifying in 3s",
|
||||||
attached_devices.len()
|
attached_devices.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Release locks before waiting
|
||||||
|
drop(wm);
|
||||||
|
drop(monitor_cache);
|
||||||
|
|
||||||
|
// Wait 3 seconds for display state to stabilize
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||||
|
|
||||||
|
// Re-query the Win32 display APIs
|
||||||
|
let re_queried_devices = match attached_display_devices(display_provider) {
|
||||||
|
Ok(devices) => devices,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("failed to re-query display devices: {}", e);
|
||||||
|
continue 'receiver;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"after verification: wm had {} monitors, initial query found {}, re-query found {}",
|
||||||
|
initial_monitor_count,
|
||||||
|
attached_devices.len(),
|
||||||
|
re_queried_devices.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// If monitors are back, the removal was transient (spurious event)
|
||||||
|
// Still try to restore state since windows might have been minimized
|
||||||
|
if re_queried_devices.len() >= initial_monitor_count {
|
||||||
|
tracing::info!(
|
||||||
|
"monitor removal was transient (spurious event), attempting state restoration. Initial: {}, Re-queried: {}",
|
||||||
|
initial_monitor_count,
|
||||||
|
re_queried_devices.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-acquire locks for state restoration
|
||||||
|
wm = wm_arc.lock();
|
||||||
|
|
||||||
|
// Update Win32 data for all monitors
|
||||||
|
for monitor in wm.monitors_mut() {
|
||||||
|
for attached in &re_queried_devices {
|
||||||
|
let serial_number_ids_match =
|
||||||
|
if let (Some(attached_snid), Some(m_snid)) =
|
||||||
|
(&attached.serial_number_id, &monitor.serial_number_id)
|
||||||
|
{
|
||||||
|
attached_snid.eq(m_snid)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if serial_number_ids_match
|
||||||
|
|| attached.device_id.eq(&monitor.device_id)
|
||||||
|
{
|
||||||
|
monitor.id = attached.id;
|
||||||
|
monitor.device = attached.device.clone();
|
||||||
|
monitor.device_id = attached.device_id.clone();
|
||||||
|
monitor.serial_number_id = attached.serial_number_id.clone();
|
||||||
|
monitor.name = attached.name.clone();
|
||||||
|
monitor.size = attached.size;
|
||||||
|
monitor.work_area_size = attached.work_area_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to restore windows that might have been minimized
|
||||||
|
let offset = wm.work_area_offset;
|
||||||
|
for monitor in wm.monitors_mut() {
|
||||||
|
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||||
|
|
||||||
|
for (idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate()
|
||||||
|
{
|
||||||
|
let is_focused_workspace = idx == focused_workspace_idx;
|
||||||
|
|
||||||
|
if is_focused_workspace {
|
||||||
|
// Restore containers
|
||||||
|
for container in workspace.containers_mut() {
|
||||||
|
if let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
|
tracing::debug!(
|
||||||
|
"restoring window after transient removal: {}",
|
||||||
|
window.hwnd
|
||||||
|
);
|
||||||
|
WindowsApi::restore_window(window.hwnd);
|
||||||
|
} else if let Some(window) = container.focused_window() {
|
||||||
|
tracing::debug!(
|
||||||
|
"skipping restore of invalid window: {}",
|
||||||
|
window.hwnd
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore maximized window
|
||||||
|
if let Some(window) = &workspace.maximized_window
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
|
WindowsApi::restore_window(window.hwnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore monocle container
|
||||||
|
if let Some(container) = &workspace.monocle_container
|
||||||
|
&& let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
|
WindowsApi::restore_window(window.hwnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore floating windows
|
||||||
|
for window in workspace.floating_windows() {
|
||||||
|
if WindowsApi::is_window(window.hwnd) {
|
||||||
|
WindowsApi::restore_window(window.hwnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor.update_focused_workspace(offset)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
border_manager::send_notification(None);
|
||||||
|
continue 'receiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If monitors are still missing, proceed with actual removal logic
|
||||||
|
tracing::info!(
|
||||||
|
"verified monitor removal ({initial_monitor_count} vs {}), removing disconnected monitors",
|
||||||
|
re_queried_devices.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-acquire locks for removal processing
|
||||||
|
wm = wm_arc.lock();
|
||||||
|
monitor_cache = MONITOR_CACHE
|
||||||
|
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
|
.lock();
|
||||||
|
|
||||||
|
// Make sure that in our state any attached displays have the latest Win32 data
|
||||||
|
// We must do this again because we dropped the lock and are working with new data
|
||||||
|
for monitor in wm.monitors_mut() {
|
||||||
|
for attached in &re_queried_devices {
|
||||||
|
let serial_number_ids_match =
|
||||||
|
if let (Some(attached_snid), Some(m_snid)) =
|
||||||
|
(&attached.serial_number_id, &monitor.serial_number_id)
|
||||||
|
{
|
||||||
|
attached_snid.eq(m_snid)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if serial_number_ids_match || attached.device_id.eq(&monitor.device_id)
|
||||||
|
{
|
||||||
|
monitor.id = attached.id;
|
||||||
|
monitor.device = attached.device.clone();
|
||||||
|
monitor.device_id = attached.device_id.clone();
|
||||||
|
monitor.serial_number_id = attached.serial_number_id.clone();
|
||||||
|
monitor.name = attached.name.clone();
|
||||||
|
monitor.size = attached.size;
|
||||||
|
monitor.work_area_size = attached.work_area_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use re-queried devices for remaining logic
|
||||||
|
re_queried_devices
|
||||||
|
} else {
|
||||||
|
attached_devices
|
||||||
|
};
|
||||||
|
|
||||||
|
if initial_monitor_count > attached_devices.len() {
|
||||||
|
tracing::info!("removing disconnected monitors");
|
||||||
|
|
||||||
// Windows to remove from `known_hwnds`
|
// Windows to remove from `known_hwnds`
|
||||||
let mut windows_to_remove = Vec::new();
|
let mut windows_to_remove = Vec::new();
|
||||||
|
|
||||||
@@ -359,13 +599,13 @@ where
|
|||||||
|
|
||||||
for (m_idx, m) in wm.monitors().iter().enumerate() {
|
for (m_idx, m) in wm.monitors().iter().enumerate() {
|
||||||
if !attached_devices.iter().any(|attached| {
|
if !attached_devices.iter().any(|attached| {
|
||||||
attached.serial_number_id().eq(m.serial_number_id())
|
attached.serial_number_id.eq(&m.serial_number_id)
|
||||||
|| attached.device_id().eq(m.device_id())
|
|| attached.device_id.eq(&m.device_id)
|
||||||
}) {
|
}) {
|
||||||
let id = m
|
let id = m
|
||||||
.serial_number_id()
|
.serial_number_id
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(m.device_id().clone(), |sn| sn.clone());
|
.map_or(m.device_id.clone(), |sn| sn.clone());
|
||||||
|
|
||||||
newly_removed_displays.push(id.clone());
|
newly_removed_displays.push(id.clone());
|
||||||
|
|
||||||
@@ -392,7 +632,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(maximized) = workspace.maximized_window() {
|
if let Some(maximized) = &workspace.maximized_window {
|
||||||
windows_to_remove.push(maximized.hwnd);
|
windows_to_remove.push(maximized.hwnd);
|
||||||
// Minimize the focused window since Windows might try
|
// Minimize the focused window since Windows might try
|
||||||
// to move it to another monitor if it was focused.
|
// to move it to another monitor if it was focused.
|
||||||
@@ -401,7 +641,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(container) = workspace.monocle_container() {
|
if let Some(container) = &workspace.monocle_container {
|
||||||
for window in container.windows() {
|
for window in container.windows() {
|
||||||
windows_to_remove.push(window.hwnd);
|
windows_to_remove.push(window.hwnd);
|
||||||
}
|
}
|
||||||
@@ -440,10 +680,10 @@ where
|
|||||||
// the user set as preference as the id.
|
// the user set as preference as the id.
|
||||||
let dip = DISPLAY_INDEX_PREFERENCES.read();
|
let dip = DISPLAY_INDEX_PREFERENCES.read();
|
||||||
let mut dip_ids = dip.values();
|
let mut dip_ids = dip.values();
|
||||||
let preferred_id = if dip_ids.any(|id| id == m.device_id()) {
|
let preferred_id = if dip_ids.any(|id| id.eq(&m.device_id)) {
|
||||||
m.device_id().clone()
|
m.device_id.clone()
|
||||||
} else if dip_ids.any(|id| Some(id) == m.serial_number_id().as_ref()) {
|
} else if dip_ids.any(|id| Some(id) == m.serial_number_id.as_ref()) {
|
||||||
m.serial_number_id().clone().unwrap_or_default()
|
m.serial_number_id.clone().unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
id
|
id
|
||||||
};
|
};
|
||||||
@@ -458,8 +698,8 @@ where
|
|||||||
// After we have cached them, remove them from our state
|
// After we have cached them, remove them from our state
|
||||||
wm.monitors_mut().retain(|m| {
|
wm.monitors_mut().retain(|m| {
|
||||||
!newly_removed_displays.iter().any(|id| {
|
!newly_removed_displays.iter().any(|id| {
|
||||||
m.serial_number_id().as_ref().is_some_and(|sn| sn == id)
|
m.serial_number_id.as_ref().is_some_and(|sn| sn == id)
|
||||||
|| m.device_id() == id
|
|| m.device_id.eq(id)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -490,7 +730,7 @@ where
|
|||||||
let post_removal_device_ids = wm
|
let post_removal_device_ids = wm
|
||||||
.monitors()
|
.monitors()
|
||||||
.iter()
|
.iter()
|
||||||
.map(Monitor::device_id)
|
.map(|m| &m.device_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@@ -513,21 +753,23 @@ where
|
|||||||
|
|
||||||
// Look in the updated state for new monitors
|
// Look in the updated state for new monitors
|
||||||
for (i, m) in wm.monitors_mut().iter_mut().enumerate() {
|
for (i, m) in wm.monitors_mut().iter_mut().enumerate() {
|
||||||
let device_id = m.device_id();
|
let device_id = &m.device_id;
|
||||||
// We identify a new monitor when we encounter a new device id
|
// We identify a new monitor when we encounter a new device id
|
||||||
if !post_removal_device_ids.contains(device_id) {
|
if !post_removal_device_ids.contains(device_id) {
|
||||||
let mut cache_hit = false;
|
let mut cache_hit = false;
|
||||||
let mut cached_id = String::new();
|
let mut cached_id = String::new();
|
||||||
// Check if that device id exists in the cache for this session
|
// Check if that device id exists in the cache for this session
|
||||||
if let Some((id, cached)) = monitor_cache.get_key_value(device_id).or(m
|
if let Some((id, cached)) = monitor_cache.get_key_value(device_id).or(m
|
||||||
.serial_number_id()
|
.serial_number_id
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|sn| monitor_cache.get_key_value(sn)))
|
.and_then(|sn| monitor_cache.get_key_value(sn)))
|
||||||
{
|
{
|
||||||
cache_hit = true;
|
cache_hit = true;
|
||||||
cached_id = id.clone();
|
cached_id = id.clone();
|
||||||
|
|
||||||
tracing::info!("found monitor and workspace configuration for {id} in the monitor cache, applying");
|
tracing::info!(
|
||||||
|
"found monitor and workspace configuration for {id} in the monitor cache, applying"
|
||||||
|
);
|
||||||
|
|
||||||
// If it does, update the cached monitor info with the new one and
|
// If it does, update the cached monitor info with the new one and
|
||||||
// load the cached monitor removing any window that has since been
|
// load the cached monitor removing any window that has since been
|
||||||
@@ -580,7 +822,9 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
if is_focused_workspace {
|
if is_focused_workspace {
|
||||||
if let Some(window) = container.focused_window() {
|
if let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"restoring window: {}",
|
"restoring window: {}",
|
||||||
window.hwnd
|
window.hwnd
|
||||||
@@ -592,7 +836,9 @@ where
|
|||||||
// first window and show that one
|
// first window and show that one
|
||||||
container.focus_window(0);
|
container.focus_window(0);
|
||||||
|
|
||||||
if let Some(window) = container.focused_window() {
|
if let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
WindowsApi::restore_window(window.hwnd);
|
WindowsApi::restore_window(window.hwnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -608,26 +854,30 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(window) = workspace.maximized_window() {
|
if let Some(window) = &workspace.maximized_window {
|
||||||
if window.exe().is_err()
|
if window.exe().is_err()
|
||||||
|| known_hwnds.contains_key(&window.hwnd)
|
|| known_hwnds.contains_key(&window.hwnd)
|
||||||
{
|
{
|
||||||
workspace.set_maximized_window(None);
|
workspace.maximized_window = None;
|
||||||
} else if is_focused_workspace {
|
} else if is_focused_workspace
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
WindowsApi::restore_window(window.hwnd);
|
WindowsApi::restore_window(window.hwnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(container) = workspace.monocle_container_mut() {
|
if let Some(container) = &mut workspace.monocle_container {
|
||||||
container.windows_mut().retain(|window| {
|
container.windows_mut().retain(|window| {
|
||||||
window.exe().is_ok()
|
window.exe().is_ok()
|
||||||
&& !known_hwnds.contains_key(&window.hwnd)
|
&& !known_hwnds.contains_key(&window.hwnd)
|
||||||
});
|
});
|
||||||
|
|
||||||
if container.windows().is_empty() {
|
if container.windows().is_empty() {
|
||||||
workspace.set_monocle_container(None);
|
workspace.monocle_container = None;
|
||||||
} else if is_focused_workspace {
|
} else if is_focused_workspace {
|
||||||
if let Some(window) = container.focused_window() {
|
if let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
WindowsApi::restore_window(window.hwnd);
|
WindowsApi::restore_window(window.hwnd);
|
||||||
} else {
|
} else {
|
||||||
// If the focused window was moved or removed by
|
// If the focused window was moved or removed by
|
||||||
@@ -635,7 +885,9 @@ where
|
|||||||
// first window and show that one
|
// first window and show that one
|
||||||
container.focus_window(0);
|
container.focus_window(0);
|
||||||
|
|
||||||
if let Some(window) = container.focused_window() {
|
if let Some(window) = container.focused_window()
|
||||||
|
&& WindowsApi::is_window(window.hwnd)
|
||||||
|
{
|
||||||
WindowsApi::restore_window(window.hwnd);
|
WindowsApi::restore_window(window.hwnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -649,7 +901,9 @@ where
|
|||||||
|
|
||||||
if is_focused_workspace {
|
if is_focused_workspace {
|
||||||
for window in workspace.floating_windows() {
|
for window in workspace.floating_windows() {
|
||||||
WindowsApi::restore_window(window.hwnd);
|
if WindowsApi::is_window(window.hwnd) {
|
||||||
|
WindowsApi::restore_window(window.hwnd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,7 +911,7 @@ where
|
|||||||
let mut workspace_matching_rules =
|
let mut workspace_matching_rules =
|
||||||
WORKSPACE_MATCHING_RULES.lock();
|
WORKSPACE_MATCHING_RULES.lock();
|
||||||
if let Some(rules) = workspace
|
if let Some(rules) = workspace
|
||||||
.workspace_config()
|
.workspace_config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|c| c.workspace_rules.as_ref())
|
.and_then(|c| c.workspace_rules.as_ref())
|
||||||
{
|
{
|
||||||
@@ -672,7 +926,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(rules) = workspace
|
if let Some(rules) = workspace
|
||||||
.workspace_config()
|
.workspace_config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|c| c.initial_workspace_rules.as_ref())
|
.and_then(|c| c.initial_workspace_rules.as_ref())
|
||||||
{
|
{
|
||||||
@@ -734,8 +988,8 @@ where
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::window_manager_event::WindowManagerEvent;
|
use crate::window_manager_event::WindowManagerEvent;
|
||||||
use crossbeam_channel::bounded;
|
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
|
use crossbeam_channel::bounded;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use windows::Win32::Devices::Display::DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY;
|
use windows::Win32::Devices::Display::DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY;
|
||||||
@@ -1006,18 +1260,18 @@ mod tests {
|
|||||||
assert_eq!(monitors.len(), 1, "Expected one monitor");
|
assert_eq!(monitors.len(), 1, "Expected one monitor");
|
||||||
|
|
||||||
// hmonitor
|
// hmonitor
|
||||||
assert_eq!(monitors[0].id(), 1);
|
assert_eq!(monitors[0].id, 1);
|
||||||
|
|
||||||
// device name
|
// device name
|
||||||
assert_eq!(monitors[0].name(), &String::from("DISPLAY1"));
|
assert_eq!(monitors[0].name, String::from("DISPLAY1"));
|
||||||
|
|
||||||
// Device
|
// Device
|
||||||
assert_eq!(monitors[0].device(), &String::from("ABC123"));
|
assert_eq!(monitors[0].device, String::from("ABC123"));
|
||||||
|
|
||||||
// Device ID
|
// Device ID
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
monitors[0].device_id(),
|
monitors[0].device_id,
|
||||||
&String::from("ABC123-4&123456&0&UID0")
|
String::from("ABC123-4&123456&0&UID0")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check monitor serial number id
|
// Check monitor serial number id
|
||||||
|
|||||||
+336
-325
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user