mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-19 14:26:58 +01:00
Compare commits
152 Commits
feature/dr
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1c9d2c468 | ||
|
|
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 | ||
|
|
ffa76ea28c | ||
|
|
2c00d79968 | ||
|
|
78177af6b8 | ||
|
|
86e0d40828 | ||
|
|
48f6ac8964 | ||
|
|
1db572f789 | ||
|
|
3dad77533b | ||
|
|
f40fb9a251 | ||
|
|
b4e16e43e9 | ||
|
|
4c2e8ff6d2 | ||
|
|
c879aae1e7 | ||
|
|
7e87e83189 | ||
|
|
aae9338f66 | ||
|
|
f68a709f1d | ||
|
|
c76846ac63 | ||
|
|
b29dd8b1d1 | ||
|
|
59c3c14731 | ||
|
|
db96f2cc5a | ||
|
|
a37a6752a8 | ||
|
|
4d0df9c5b5 | ||
|
|
f8ea62f857 | ||
|
|
93bb41737b | ||
|
|
280352eeef | ||
|
|
7619b9b4ed | ||
|
|
72a4d5276e | ||
|
|
4bb3b83d57 | ||
|
|
ccd2f3a464 | ||
|
|
7e242ada66 | ||
|
|
5b2acd0f12 | ||
|
|
ec0bbaae98 | ||
|
|
3c44f3bfdb | ||
|
|
7839980ddf | ||
|
|
6416c0b6eb | ||
|
|
f6ccec9505 | ||
|
|
21cb5e1e6f | ||
|
|
009c0dcd28 | ||
|
|
98c5ab3b9b | ||
|
|
d4eeec994f | ||
|
|
e9ed1cfd3b | ||
|
|
4a2eb391f7 | ||
|
|
41e18bccc6 | ||
|
|
3d373b3630 | ||
|
|
b4e61b079c | ||
|
|
eec6312a51 | ||
|
|
00384ce333 | ||
|
|
e75578d9ce | ||
|
|
50c850cb02 | ||
|
|
5a1af5b133 | ||
|
|
74c433185a | ||
|
|
71bb346c89 | ||
|
|
3feff1dca9 | ||
|
|
3019eaf89c | ||
|
|
ce59bd9ae4 | ||
|
|
309dd159ca | ||
|
|
6f1d6dbdc7 | ||
|
|
6a10d583a6 | ||
|
|
270ea5aa46 | ||
|
|
a9d2738733 | ||
|
|
3d8f68e559 | ||
|
|
80bb7288c4 | ||
|
|
76c833f661 | ||
|
|
80bce4be7e | ||
|
|
8c10547325 | ||
|
|
8f886b3fe4 | ||
|
|
53c38e157f | ||
|
|
70f561e6ac | ||
|
|
4ea835fa59 | ||
|
|
09137af305 | ||
|
|
ee89b344df | ||
|
|
46d5ea4a1d | ||
|
|
82c2241601 | ||
|
|
c28773b96a | ||
|
|
577364a556 | ||
|
|
17cd0308cb | ||
|
|
10424b696f | ||
|
|
6e7d8fb922 | ||
|
|
917cd9b7db | ||
|
|
bdbd665b21 | ||
|
|
f3f2098451 | ||
|
|
4ca2e8388b | ||
|
|
31752e422a | ||
|
|
5e308b9131 | ||
|
|
1bf53b89af | ||
|
|
11690c6004 | ||
|
|
3457dfc04c | ||
|
|
af1c9b5aa9 | ||
|
|
22fac5a9fb | ||
|
|
7a3990f106 | ||
|
|
74e93e5524 | ||
|
|
86e78570d6 | ||
|
|
3ee3aac806 | ||
|
|
c408c1149c | ||
|
|
a8b02f40fd | ||
|
|
6608e5a5bb | ||
|
|
8ef1bcf26e | ||
|
|
d146f35c25 | ||
|
|
fc07ba3dd9 | ||
|
|
3a8a61119d | ||
|
|
d24beb60b1 | ||
|
|
7daf3242e2 | ||
|
|
b6e261aef6 | ||
|
|
d40c304324 | ||
|
|
69d252ba12 | ||
|
|
2ac477d89f |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -55,6 +55,7 @@ body:
|
||||
label: Hotkey Configuration
|
||||
description: >
|
||||
Please provide your whkdrc or komorebi.ahk hotkey configuration file
|
||||
render: shell
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
@@ -62,3 +63,4 @@ body:
|
||||
label: Output of komorebic check
|
||||
description: >
|
||||
Please provide the output of `komorebic check`
|
||||
render: shell
|
||||
|
||||
2
.github/workflows/feature-check.yaml
vendored
2
.github/workflows/feature-check.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check and close feature issues
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
23
.github/workflows/windows.yaml
vendored
23
.github/workflows/windows.yaml
vendored
@@ -13,15 +13,15 @@ on:
|
||||
- hotfix/*
|
||||
tags:
|
||||
- v*
|
||||
schedule:
|
||||
- cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
# schedule:
|
||||
# - cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
@@ -43,10 +43,11 @@ jobs:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: rustup toolchain install stable --profile minimal
|
||||
- run: rustup component add --toolchain stable-x86_64-pc-windows-msvc clippy
|
||||
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -64,7 +65,7 @@ jobs:
|
||||
- run: |
|
||||
cargo install cargo-wix
|
||||
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
|
||||
path: |
|
||||
@@ -81,12 +82,12 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v6
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
@@ -128,14 +129,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v6
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
@@ -170,14 +171,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v6
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
|
||||
3700
Cargo.lock
generated
3700
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -8,7 +8,8 @@ members = [
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes"
|
||||
"komorebi-themes",
|
||||
"komorebi-shortcuts",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -18,8 +19,8 @@ chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.31"
|
||||
egui_extras = "0.31"
|
||||
eframe = "0.33"
|
||||
egui_extras = "0.33"
|
||||
dirs = "6"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
@@ -32,19 +33,20 @@ strum = { version = "0.27", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
parking_lot = "0.12"
|
||||
paste = "1"
|
||||
sysinfo = "0.33"
|
||||
sysinfo = "0.37"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "a28c6559a9de2f92c142a714947a9b081776caca" }
|
||||
windows-numerics = { version = "0.2" }
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
|
||||
windows-numerics = { version = "0.3" }
|
||||
windows-implement = { version = "0.60" }
|
||||
windows-interface = { version = "0.59" }
|
||||
windows-core = { version = "0.61" }
|
||||
windows-core = { version = "0.62" }
|
||||
shadow-rs = "1"
|
||||
which = "7"
|
||||
which = "8"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.61"
|
||||
version = "0.62"
|
||||
features = [
|
||||
"Foundation_Numerics",
|
||||
"Win32_Devices",
|
||||
@@ -71,5 +73,12 @@ features = [
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control"
|
||||
]
|
||||
"Media_Control",
|
||||
]
|
||||
|
||||
[profile.release-opt]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
33
README.md
33
README.md
@@ -29,6 +29,37 @@ 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: komorebi for Mac
|
||||
|
||||
If you made your way to this repo looking for [komorebi for
|
||||
Mac](https://github.com/KomoCorp/komorebi-for-mac), the project is currently
|
||||
being developed in private with [early access available to GitHub
|
||||
Sponsors](https://github.com/sponsors/LGUG2Z).
|
||||
|
||||
If you want to see how far along development is before signing up for early
|
||||
access (spoiler: it's very far along!) there is an overview video you can watch
|
||||
[here](https://www.youtube.com/watch?v=u3eJcsa_MJk).
|
||||
|
||||
Sponsors with early access can install komorebi for Mac either by compiling
|
||||
from source, by using Homebrew, or by using the project's Nix Flake.
|
||||
|
||||
## Overview
|
||||
|
||||
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
|
||||
@@ -394,7 +425,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.35"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.39"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
|
||||
44
deny.toml
44
deny.toml
@@ -14,13 +14,15 @@ feature-depth = 1
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" }
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
|
||||
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" }
|
||||
]
|
||||
|
||||
[licenses]
|
||||
allow = [
|
||||
"0BSD",
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"Artistic-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
@@ -34,43 +36,58 @@ allow = [
|
||||
"Ubuntu-font-1.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"LicenseRef-Komorebi-1.0"
|
||||
"LicenseRef-Komorebi-2.0"
|
||||
]
|
||||
confidence-threshold = 0.8
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-client"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebic"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebic-no-console"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-themes"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-gui"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-bar"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-shortcuts"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "whkd-core"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "whkd-parser"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
@@ -78,6 +95,11 @@ crate = "base16-egui-themes"
|
||||
expression = "MIT"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "win32-display-data"
|
||||
expression = "0BSD"
|
||||
license-files = []
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
wildcards = "allow"
|
||||
@@ -91,9 +113,11 @@ unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = [
|
||||
"https://github.com/LGUG2Z/base16-egui-themes",
|
||||
"https://github.com/LGUG2Z/catppuccin-egui",
|
||||
"https://github.com/LGUG2Z/windows-icons",
|
||||
"https://github.com/LGUG2Z/win32-display-data",
|
||||
"https://github.com/LGUG2Z/flavours",
|
||||
"https://github.com/LGUG2Z/base16_color_scheme",
|
||||
"https://github.com/LGUG2Z/whkd",
|
||||
"https://github.com/LGUG2Z/catppuccin-egui",
|
||||
"https://github.com/amPerl/egui-phosphor",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,8 @@ Options:
|
||||
Desired ease function for animation
|
||||
|
||||
[default: linear]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint,
|
||||
ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo,
|
||||
ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
|
||||
-a, --animation-type <ANIMATION_TYPE>
|
||||
Animation type to apply the style to. If not specified, sets global style
|
||||
|
||||
12
docs/cli/cancel-preselect.md
Normal file
12
docs/cli/cancel-preselect.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# cancel-preselect
|
||||
|
||||
```
|
||||
Cancel a workspace preselect set by the preselect-direction command, if one exists
|
||||
|
||||
Usage: komorebic.exe cancel-preselect
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
|
||||
|
||||
Arguments:
|
||||
<DEFAULT_LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -12,9 +12,6 @@ Options:
|
||||
--whkd
|
||||
Enable autostart of whkd
|
||||
|
||||
--ahk
|
||||
Enable autostart of ahk
|
||||
|
||||
--bar
|
||||
Enable autostart of komorebi-bar
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@ Options:
|
||||
--whkd
|
||||
Kill whkd if it is running as a background process
|
||||
|
||||
--ahk
|
||||
Kill ahk if it is running as a background process
|
||||
|
||||
--bar
|
||||
Kill komorebi-bar if it is running as a background process
|
||||
|
||||
|
||||
16
docs/cli/license.md
Normal file
16
docs/cli/license.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# license
|
||||
|
||||
```
|
||||
Specify an email associated with an Individual Commercial Use License
|
||||
|
||||
Usage: komorebic.exe license <EMAIL>
|
||||
|
||||
Arguments:
|
||||
<EMAIL>
|
||||
Email address associated with an Individual Commercial Use License
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -10,7 +10,7 @@ Arguments:
|
||||
Target workspace name
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
16
docs/cli/preselect-direction.md
Normal file
16
docs/cli/preselect-direction.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# preselect-direction
|
||||
|
||||
```
|
||||
Preselect the specified direction for the next window to be spawned on supported layouts
|
||||
|
||||
Usage: komorebic.exe preselect-direction <OPERATION_DIRECTION>
|
||||
|
||||
Arguments:
|
||||
<OPERATION_DIRECTION>
|
||||
[possible values: left, right, up, down]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/promote-swap.md
Normal file
12
docs/cli/promote-swap.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# promote-swap
|
||||
|
||||
```
|
||||
Promote the focused window to the largest tile by swapping container indices with the largest tile
|
||||
|
||||
Usage: komorebic.exe promote-swap
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# promote
|
||||
|
||||
```
|
||||
Promote the focused window to the top of the tree
|
||||
Promote the focused window to the largest tile via container removal and re-insertion
|
||||
|
||||
Usage: komorebic.exe promote
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe query <STATE_QUERY>
|
||||
|
||||
Arguments:
|
||||
<STATE_QUERY>
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, version]
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, focused-container-kind, version]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
16
docs/cli/scrolling-layout-columns.md
Normal file
16
docs/cli/scrolling-layout-columns.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# scrolling-layout-columns
|
||||
|
||||
```
|
||||
Set the number of visible columns for the Scrolling layout on the focused workspace
|
||||
|
||||
Usage: komorebic.exe scrolling-layout-columns <COUNT>
|
||||
|
||||
Arguments:
|
||||
<COUNT>
|
||||
Desired number of visible columns
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -18,9 +18,6 @@ Options:
|
||||
--whkd
|
||||
Start whkd in a background process
|
||||
|
||||
--ahk
|
||||
Start autohotkey configuration file
|
||||
|
||||
--bar
|
||||
Start komorebi-bar in a background process
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@ Options:
|
||||
--whkd
|
||||
Stop whkd if it is running as a background process
|
||||
|
||||
--ahk
|
||||
Stop ahk if it is running as a background process
|
||||
|
||||
--bar
|
||||
Stop komorebi-bar if it is running as a background process
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# toggle-pause
|
||||
|
||||
```
|
||||
Toggle window tiling on the focused workspace
|
||||
Toggle the paused state for all window tiling
|
||||
|
||||
Usage: komorebic.exe toggle-pause
|
||||
|
||||
|
||||
12
docs/cli/toggle-shortcuts.md
Normal file
12
docs/cli/toggle-shortcuts.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-shortcuts
|
||||
|
||||
```
|
||||
Toggle the komorebi-shortcuts helper
|
||||
|
||||
Usage: komorebic.exe toggle-shortcuts
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -8,7 +8,7 @@ Usage: komorebic.exe window-hiding-behaviour <HIDING_BEHAVIOUR>
|
||||
Arguments:
|
||||
<HIDING_BEHAVIOUR>
|
||||
Possible values:
|
||||
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
- hide: END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
31
docs/cli/workspace-work-area-offset.md
Normal file
31
docs/cli/workspace-work-area-offset.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# workspace-work-area-offset
|
||||
|
||||
```
|
||||
Set offsets for a workspace to exclude parts of the work area from tiling
|
||||
|
||||
Usage: komorebic.exe workspace-work-area-offset <MONITOR> <WORKSPACE> <LEFT> <TOP> <RIGHT> <BOTTOM>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index (zero-indexed)
|
||||
|
||||
<LEFT>
|
||||
Size of the left work area offset (set right to left * 2 to maintain right padding)
|
||||
|
||||
<TOP>
|
||||
Size of the top work area offset (set bottom to the same value to maintain bottom padding)
|
||||
|
||||
<RIGHT>
|
||||
Size of the right work area offset
|
||||
|
||||
<BOTTOM>
|
||||
Size of the bottom work area offset
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -6,7 +6,10 @@ defined in the `komorebi.json` configuration file.
|
||||
```json
|
||||
{
|
||||
"animation": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"duration": 250,
|
||||
"fps": 60,
|
||||
"style": "EaseOutSine"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -301,7 +301,7 @@ how to map the indices and would use default behaviour which would result in a m
|
||||
}
|
||||
```
|
||||
|
||||
# Multiple Monitors on different machines
|
||||
# Multiple monitors on different machines
|
||||
|
||||
You can use the same `komorebi.json` to configure two different setups and then synchronize your config across machines.
|
||||
However, if you do this it is important to be aware of a few things.
|
||||
@@ -393,6 +393,13 @@ This is because komorebi will apply the appropriate config to the loaded monitor
|
||||
index (the index defined in the user config) to the actual monitor index, and the bar will use that map to know if it
|
||||
should be enabled, and where it should be drawn.
|
||||
|
||||
# Windows Display Settings
|
||||
|
||||
In `Settings > System > Display > Multiple Displays`:
|
||||
|
||||
- Disable "Remember windows locations on monitor connection"
|
||||
- Enable "Minimize windows when a monitor is disconnected"
|
||||
|
||||
### Things to keep in mind
|
||||
|
||||
* If you are using a laptop connected to one monitor at work and a different one at home, the work monitor and the home
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Setting a Given Display to a Specific Index
|
||||
|
||||
If you would like `komorebi` to remember monitor index positions, you will need to set the `display_index_preferences`
|
||||
configuration option in the static configuration file.
|
||||
|
||||
Display IDs can be found using `komorebic monitor-information`.
|
||||
|
||||
Then, in `komorebi.json`, you simply need to specify the preferred index position for each display ID:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "DEL4310-5&1a6c0954&0&UID209155",
|
||||
"1": "<another-display_id>"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -16,6 +16,19 @@ the example files have been downloaded. For most new users this will be in the
|
||||
komorebic quickstart
|
||||
```
|
||||
|
||||
## Corporate Devices Enrolled in MDM
|
||||
|
||||
If you are using `komorebi` on a corporate device enrolled in mobile device
|
||||
management, you will receive a pop-up when you run `komorebic start` reminding
|
||||
you that the [Komorebi License](https://github.com/LGUG2Z/komorebi-license) does
|
||||
not permit any kind of commercial use.
|
||||
|
||||
You can remove this pop-up by running `komorebic license <email>` with the email
|
||||
associated with your Individual Commercial Use License. A single HTTP request
|
||||
will be sent with the given email address to verify license validity.
|
||||
|
||||
## Starting komorebi
|
||||
|
||||
With the example configurations downloaded, you can now start `komorebi`,
|
||||
`komorebi-bar` and `whkd`.
|
||||
|
||||
@@ -23,6 +36,9 @@ With the example configurations downloaded, you can now start `komorebi`,
|
||||
komorebic start --whkd --bar
|
||||
```
|
||||
|
||||
If you don't want to use the komorebi status bar, you can remove the `--bar` option
|
||||
from the above command.
|
||||
|
||||
## komorebi.json
|
||||
|
||||
The example window manager configuration sets some sane defaults and provides
|
||||
@@ -186,6 +202,9 @@ limitations on hotkey bindings that include the `win` key. However, you will sti
|
||||
to [modify the registry](https://superuser.com/questions/1059511/how-to-disable-winl-in-windows-10) to prevent
|
||||
`win + l` from locking the operating system.
|
||||
|
||||
You can toggle an overlay of the current `whkdrc` shortcuts related to `komorebi` at any time when using the example
|
||||
configuration with `alt + i`.
|
||||
|
||||
```
|
||||
{% include "./whkdrc.sample" %}
|
||||
```
|
||||
|
||||
@@ -120,6 +120,7 @@ cargo +stable install --path komorebic --locked
|
||||
cargo +stable install --path komorebic-no-console --locked
|
||||
cargo +stable install --path komorebi-gui --locked
|
||||
cargo +stable install --path komorebi-bar --locked
|
||||
cargo +stable install --path komorebi-shortcuts --locked
|
||||
```
|
||||
|
||||
If the binaries have been built and added to your `$PATH` correctly, you should
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.35/schema.bar.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.bar.json",
|
||||
"monitor": 0,
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
@@ -48,8 +48,8 @@
|
||||
{
|
||||
"Network": {
|
||||
"enable": true,
|
||||
"show_total_data_transmitted": true,
|
||||
"show_network_activity": true
|
||||
"show_activity": true,
|
||||
"show_total_activity": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.35/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
@@ -14,13 +14,6 @@
|
||||
"unfocused_border": "Base03",
|
||||
"bar_accent": "Base0D"
|
||||
},
|
||||
"stackbar": {
|
||||
"height": 40,
|
||||
"mode": "OnStack",
|
||||
"tabs": {
|
||||
"width": 300
|
||||
}
|
||||
},
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
|
||||
@@ -138,13 +138,14 @@ running `komorebic stop` and `komorebic start`.
|
||||
Users with Nvidia GPUs may have issues with transparency on the Komorebi Bar.
|
||||
|
||||
To solve this the user can do the following:
|
||||
1. Open the Nvidia Control Panel
|
||||
2. On the left menu tree, under "3D Settings", select "Manage 3D Settings"
|
||||
3. Select the "Program Settings" tab
|
||||
4. Press the "Add" button and select "komorebi-bar"
|
||||
5. Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
|
||||
6. Change the setting to "Prefer compatibility"
|
||||
7. At the bottom of the window select "Apply"
|
||||
8. Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
|
||||
|
||||
- Open the Nvidia Control Panel
|
||||
- On the left menu tree, under "3D Settings", select "Manage 3D Settings"
|
||||
- Select the "Program Settings" tab
|
||||
- Press the "Add" button and select "komorebi-bar"
|
||||
- Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
|
||||
- Change the setting to "Prefer compatibility"
|
||||
- At the bottom of the window select "Apply"
|
||||
- Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
|
||||
|
||||
This should resolve the issue and your Komorebi Bar should render with the proper transparency.
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
alt + o : taskkill /f /im whkd.exe; Start-Process whkd -WindowStyle hidden # if shell is pwsh / powershell
|
||||
alt + shift + o : komorebic reload-configuration
|
||||
|
||||
alt + i : komorebic toggle-shortcuts
|
||||
|
||||
# App shortcuts - these require shell to be pwsh / powershell
|
||||
# The apps will be focused if open, or launched if not open
|
||||
# alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox }
|
||||
|
||||
15
justfile
15
justfile
@@ -15,6 +15,9 @@ fmt:
|
||||
prettier --write .github/FUNDING.yml
|
||||
prettier --write .github/workflows/windows.yaml
|
||||
|
||||
fix:
|
||||
cargo clippy --fix --allow-dirty
|
||||
|
||||
install-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
|
||||
|
||||
@@ -28,10 +31,10 @@ install-target-with-jsonschema target:
|
||||
cargo +stable install --path {{ target }} --locked
|
||||
|
||||
install:
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
|
||||
install-with-jsonschema:
|
||||
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
|
||||
build-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just build-target $_ }
|
||||
@@ -40,7 +43,7 @@ build-target target:
|
||||
cargo +stable build --package {{ target }} --locked --release --no-default-features
|
||||
|
||||
build:
|
||||
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
|
||||
copy-target target:
|
||||
cp .\target\release\{{ target }}.exe $Env:USERPROFILE\.cargo\bin
|
||||
@@ -52,7 +55,7 @@ wpm target:
|
||||
just build-target {{ target }} && wpmctl stop {{ target }}; just copy-target {{ target }} && wpmctl start {{ target }}
|
||||
|
||||
copy:
|
||||
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
|
||||
run target:
|
||||
cargo +stable run --bin {{ target }} --locked --no-default-features
|
||||
@@ -88,3 +91,7 @@ schemagen:
|
||||
generate-schema-doc ./schema.json --config template_name=js_offline --config minify=false ./static-config-docs/
|
||||
generate-schema-doc ./schema.bar.json --config template_name=js_offline --config minify=false ./bar-config-docs/
|
||||
mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html
|
||||
|
||||
depgen:
|
||||
cargo deny check
|
||||
cargo deny list --format json | jq 'del(.unlicensed)' > dependencies.json
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
komorebi-themes = { path = "../komorebi-themes", default-features = false }
|
||||
|
||||
chrono-tz = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
@@ -17,15 +17,16 @@ crossbeam-channel = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui-phosphor = "0.9"
|
||||
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" }
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
lazy_static = { workspace = true }
|
||||
netdev = "0.33"
|
||||
netdev = "0.39"
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
parking_lot = { workspace = true }
|
||||
random_word = { version = "0.5", features = ["en"] }
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
schemars = { workspace = true, optional = true }
|
||||
@@ -35,6 +36,7 @@ starship-battery = "0.10"
|
||||
sysinfo = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
which = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" }
|
||||
@@ -42,4 +44,4 @@ windows-icons-fallback = { package = "windows-icons", git = "https://github.com/
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["dep:schemars"]
|
||||
schemars = ["dep:schemars", "komorebi-client/default", "komorebi-themes/default"]
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
use crate::config::get_individual_spacing;
|
||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||
use crate::BAR_HEIGHT;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use crate::KomorebiEvent;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_LEFT;
|
||||
use crate::MONITOR_RIGHT;
|
||||
use crate::MONITOR_TOP;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use crate::config::get_individual_spacing;
|
||||
use crate::process_hwnd;
|
||||
use crate::render::Color32Ext;
|
||||
use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::render::RenderExt;
|
||||
use crate::widgets::komorebi::Komorebi;
|
||||
use crate::widgets::komorebi::KomorebiNotificationState;
|
||||
use crate::widgets::komorebi::MonitorInfo;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::widgets::widget::WidgetConfig;
|
||||
use crate::KomorebiEvent;
|
||||
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 color_eyre::eyre;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use eframe::egui::Align;
|
||||
@@ -38,6 +39,7 @@ use eframe::egui::Frame;
|
||||
use eframe::egui::Id;
|
||||
use eframe::egui::Layout;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::PointerButton;
|
||||
use eframe::egui::Rgba;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::TextStyle;
|
||||
@@ -46,22 +48,98 @@ use eframe::egui::Visuals;
|
||||
use font_loader::system_fonts;
|
||||
use font_loader::system_fonts::FontPropertyBuilder;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::MonitorNotification;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use komorebi_themes::Base16Value;
|
||||
use komorebi_client::VirtualDesktopNotification;
|
||||
use komorebi_themes::Base16Wrapper;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::CatppuccinValue;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Error;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Write;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ChildStdin;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
lazy_static! {
|
||||
static ref SESSION_STDIN: Mutex<Option<ChildStdin>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
fn start_powershell() -> eyre::Result<()> {
|
||||
// found running session, do nothing
|
||||
if SESSION_STDIN.lock().as_mut().is_some() {
|
||||
tracing::debug!("PowerShell session already started");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::debug!("Starting PowerShell session");
|
||||
|
||||
let mut child = Command::new("powershell.exe")
|
||||
.args(["-NoLogo", "-NoProfile", "-Command", "-"])
|
||||
.stdin(Stdio::piped())
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().expect("stdin piped");
|
||||
|
||||
// Store stdin for later commands
|
||||
let mut session_stdin = SESSION_STDIN.lock();
|
||||
*session_stdin = Option::from(stdin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_powershell() -> eyre::Result<()> {
|
||||
tracing::debug!("Stopping PowerShell session");
|
||||
|
||||
if let Some(mut session_stdin) = SESSION_STDIN.lock().take() {
|
||||
if let Err(e) = session_stdin.write_all(b"exit\n") {
|
||||
tracing::error!(error = %e, "failed to write exit command to PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
if let Err(e) = session_stdin.flush() {
|
||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
tracing::debug!("PowerShell session stopped");
|
||||
} else {
|
||||
tracing::debug!("PowerShell session already stopped");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_powershell(cmd: &str) -> eyre::Result<()> {
|
||||
if let Some(session_stdin) = SESSION_STDIN.lock().as_mut() {
|
||||
if let Err(e) = writeln!(session_stdin, "{cmd}") {
|
||||
tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
if let Err(e) = session_stdin.flush() {
|
||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(Error::new(ErrorKind::NotFound, "PowerShell session not started").into())
|
||||
}
|
||||
|
||||
pub struct Komobar {
|
||||
pub hwnd: Option<isize>,
|
||||
@@ -69,7 +147,7 @@ pub struct Komobar {
|
||||
pub disabled: bool,
|
||||
pub config: KomobarConfig,
|
||||
pub render_config: Rc<RefCell<RenderConfig>>,
|
||||
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
pub monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub center_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
||||
@@ -81,6 +159,18 @@ pub struct Komobar {
|
||||
pub size_rect: komorebi_client::Rect,
|
||||
pub work_area_offset: komorebi_client::Rect,
|
||||
applied_theme_on_first_frame: bool,
|
||||
mouse_follows_focus: bool,
|
||||
input_config: InputConfig,
|
||||
}
|
||||
|
||||
struct InputConfig {
|
||||
accumulated_scroll_delta: Vec2,
|
||||
act_on_vertical_scroll: bool,
|
||||
act_on_horizontal_scroll: bool,
|
||||
vertical_scroll_threshold: f32,
|
||||
horizontal_scroll_threshold: f32,
|
||||
vertical_scroll_max_threshold: f32,
|
||||
horizontal_scroll_max_threshold: f32,
|
||||
}
|
||||
|
||||
pub fn apply_theme(
|
||||
@@ -229,16 +319,15 @@ pub fn apply_theme(
|
||||
// apply rounding to the widgets
|
||||
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) =
|
||||
&grouping
|
||||
&& let Some(rounding) = config.rounding
|
||||
{
|
||||
if let Some(rounding) = config.rounding {
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
}
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
}
|
||||
|
||||
// Update RenderConfig's background_color so that widgets will have the new color
|
||||
@@ -249,7 +338,7 @@ impl Komobar {
|
||||
pub fn apply_config(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
previous_monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
||||
) {
|
||||
MAX_LABEL_WIDTH.store(
|
||||
self.config.max_label_width.unwrap_or(400.0) as i32,
|
||||
@@ -278,7 +367,7 @@ impl Komobar {
|
||||
self.config.icon_scale,
|
||||
));
|
||||
|
||||
let mut komorebi_notification_state = previous_notification_state;
|
||||
let mut monitor_info = previous_monitor_info;
|
||||
let mut komorebi_widgets = Vec::new();
|
||||
|
||||
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
|
||||
@@ -330,19 +419,18 @@ impl Komobar {
|
||||
komorebi_widgets
|
||||
.into_iter()
|
||||
.for_each(|(mut widget, idx, side)| {
|
||||
match komorebi_notification_state {
|
||||
match monitor_info {
|
||||
None => {
|
||||
komorebi_notification_state =
|
||||
Some(widget.komorebi_notification_state.clone());
|
||||
monitor_info = Some(widget.monitor_info.clone());
|
||||
}
|
||||
Some(ref previous) => {
|
||||
if widget.workspaces.is_some_and(|w| w.enable) {
|
||||
previous.borrow_mut().update_from_config(
|
||||
&widget.komorebi_notification_state.borrow(),
|
||||
);
|
||||
if widget.workspaces.is_some() {
|
||||
previous
|
||||
.borrow_mut()
|
||||
.update_from_self(&widget.monitor_info.borrow());
|
||||
}
|
||||
|
||||
widget.komorebi_notification_state = previous.clone();
|
||||
widget.monitor_info = previous.clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,15 +455,19 @@ impl Komobar {
|
||||
}
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
};
|
||||
let monitor_index = self.komorebi_notification_state.as_ref().and_then(|state| {
|
||||
state
|
||||
.borrow()
|
||||
.monitor_usr_idx_map
|
||||
.get(&usr_monitor_index)
|
||||
.copied()
|
||||
|
||||
let mapped_info = self.monitor_info.as_ref().map(|info| {
|
||||
let monitor = info.borrow();
|
||||
(
|
||||
monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
|
||||
monitor.mouse_follows_focus,
|
||||
)
|
||||
});
|
||||
|
||||
self.monitor_index = monitor_index;
|
||||
if let Some(info) = mapped_info {
|
||||
self.monitor_index = info.0;
|
||||
self.mouse_follows_focus = info.1;
|
||||
}
|
||||
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
|
||||
@@ -426,17 +518,51 @@ impl Komobar {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.komorebi_notification_state.is_some() && !self.disabled {
|
||||
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
|
||||
} else if self.monitor_info.is_some() && !self.disabled {
|
||||
tracing::warn!(
|
||||
"couldn't find the monitor index of this bar! Disabling the bar until the monitor connects..."
|
||||
);
|
||||
self.disabled = true;
|
||||
} else {
|
||||
tracing::warn!("couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi.");
|
||||
tracing::warn!(
|
||||
"couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi."
|
||||
);
|
||||
self.disabled = true;
|
||||
}
|
||||
|
||||
if let Some(mouse) = &self.config.mouse {
|
||||
self.input_config.act_on_vertical_scroll =
|
||||
mouse.on_scroll_up.is_some() || mouse.on_scroll_down.is_some();
|
||||
self.input_config.act_on_horizontal_scroll =
|
||||
mouse.on_scroll_left.is_some() || mouse.on_scroll_right.is_some();
|
||||
self.input_config.vertical_scroll_threshold = mouse
|
||||
.vertical_scroll_threshold
|
||||
.unwrap_or(30.0)
|
||||
.clamp(10.0, 300.0);
|
||||
self.input_config.horizontal_scroll_threshold = mouse
|
||||
.horizontal_scroll_threshold
|
||||
.unwrap_or(30.0)
|
||||
.clamp(10.0, 300.0);
|
||||
// limit how many "ticks" can be accumulated
|
||||
self.input_config.vertical_scroll_max_threshold =
|
||||
self.input_config.vertical_scroll_threshold * 3.0;
|
||||
self.input_config.horizontal_scroll_max_threshold =
|
||||
self.input_config.horizontal_scroll_threshold * 3.0;
|
||||
|
||||
if mouse.has_command() {
|
||||
start_powershell().unwrap_or_else(|_| {
|
||||
tracing::error!("failed to start powershell session");
|
||||
});
|
||||
} else {
|
||||
stop_powershell().unwrap_or_else(|_| {
|
||||
tracing::error!("failed to stop powershell session");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("widget configuration options applied");
|
||||
|
||||
self.komorebi_notification_state = komorebi_notification_state;
|
||||
self.monitor_info = monitor_info;
|
||||
}
|
||||
|
||||
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
|
||||
@@ -473,7 +599,9 @@ impl Komobar {
|
||||
end.x -= margin.left + margin.right;
|
||||
|
||||
if end.y == 0.0 {
|
||||
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
|
||||
tracing::warn!(
|
||||
"position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default"
|
||||
)
|
||||
}
|
||||
|
||||
self.size_rect = komorebi_client::Rect {
|
||||
@@ -505,8 +633,7 @@ impl Komobar {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
home
|
||||
@@ -520,26 +647,6 @@ impl Komobar {
|
||||
match komorebi_client::StaticConfig::read(&config) {
|
||||
Ok(config) => {
|
||||
if let Some(theme) = config.theme {
|
||||
let stack_accent = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name, stack_border, ..
|
||||
} => stack_border
|
||||
.unwrap_or(CatppuccinValue::Green)
|
||||
.color32(name.as_theme()),
|
||||
KomorebiTheme::Base16 {
|
||||
name, stack_border, ..
|
||||
} => stack_border
|
||||
.unwrap_or(Base16Value::Base0B)
|
||||
.color32(Base16Wrapper::Base16(name)),
|
||||
KomorebiTheme::Custom {
|
||||
ref colours,
|
||||
stack_border,
|
||||
..
|
||||
} => stack_border
|
||||
.unwrap_or(Base16Value::Base0B)
|
||||
.color32(Base16Wrapper::Custom(colours.clone())),
|
||||
};
|
||||
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
@@ -549,10 +656,6 @@ impl Komobar {
|
||||
bar_grouping,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
|
||||
if let Some(state) = &self.komorebi_notification_state {
|
||||
state.borrow_mut().stack_accent = Some(stack_accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -565,17 +668,16 @@ impl Komobar {
|
||||
| Grouping::Alignment(config)
|
||||
| Grouping::Widget(config),
|
||||
) = &bar_grouping
|
||||
&& let Some(rounding) = config.rounding
|
||||
{
|
||||
if let Some(rounding) = config.rounding {
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius =
|
||||
rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
}
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius =
|
||||
rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,7 +697,7 @@ impl Komobar {
|
||||
disabled: false,
|
||||
config,
|
||||
render_config: Rc::new(RefCell::new(RenderConfig::new())),
|
||||
komorebi_notification_state: None,
|
||||
monitor_info: None,
|
||||
left_widgets: vec![],
|
||||
center_widgets: vec![],
|
||||
right_widgets: vec![],
|
||||
@@ -607,6 +709,16 @@ impl Komobar {
|
||||
size_rect: komorebi_client::Rect::default(),
|
||||
work_area_offset: komorebi_client::Rect::default(),
|
||||
applied_theme_on_first_frame: false,
|
||||
mouse_follows_focus: false,
|
||||
input_config: InputConfig {
|
||||
accumulated_scroll_delta: Vec2::new(0.0, 0.0),
|
||||
act_on_vertical_scroll: false,
|
||||
act_on_horizontal_scroll: false,
|
||||
vertical_scroll_threshold: 0.0,
|
||||
horizontal_scroll_threshold: 0.0,
|
||||
vertical_scroll_max_threshold: 0.0,
|
||||
horizontal_scroll_max_threshold: 0.0,
|
||||
},
|
||||
};
|
||||
|
||||
komobar.apply_config(&cc.egui_ctx, None);
|
||||
@@ -731,12 +843,12 @@ impl eframe::App for Komobar {
|
||||
|
||||
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
|
||||
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
}
|
||||
|
||||
if let Ok(updated_config) = self.rx_config.try_recv() {
|
||||
self.config = updated_config;
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
}
|
||||
|
||||
match self.rx_gui.try_recv() {
|
||||
@@ -758,6 +870,30 @@ impl eframe::App for Komobar {
|
||||
self.monitor_index = monitor_index;
|
||||
let mut should_apply_config = false;
|
||||
|
||||
match notification.event {
|
||||
NotificationEvent::VirtualDesktop(
|
||||
VirtualDesktopNotification::EnteredAssociatedVirtualDesktop,
|
||||
) => {
|
||||
tracing::debug!(
|
||||
"back on komorebi's associated virtual desktop - restoring bar"
|
||||
);
|
||||
if let Some(hwnd) = self.hwnd {
|
||||
komorebi_client::WindowsApi::restore_window(hwnd);
|
||||
}
|
||||
}
|
||||
NotificationEvent::VirtualDesktop(
|
||||
VirtualDesktopNotification::LeftAssociatedVirtualDesktop,
|
||||
) => {
|
||||
tracing::debug!(
|
||||
"no longer on komorebi's associated virtual desktop - minimizing bar"
|
||||
);
|
||||
if let Some(hwnd) = self.hwnd {
|
||||
komorebi_client::WindowsApi::minimize_window(hwnd);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.monitor_index.is_none()
|
||||
|| self
|
||||
.monitor_index
|
||||
@@ -802,9 +938,9 @@ impl eframe::App for Komobar {
|
||||
) {
|
||||
let monitor_index = self.monitor_index.expect("should have a monitor index");
|
||||
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size();
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size;
|
||||
|
||||
self.update_monitor_coordinates(monitor_size);
|
||||
self.update_monitor_coordinates(&monitor_size);
|
||||
|
||||
should_apply_config = true;
|
||||
}
|
||||
@@ -815,7 +951,7 @@ impl eframe::App for Komobar {
|
||||
|
||||
// Check if monitor coordinates/size has changed
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size();
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size;
|
||||
let top = MONITOR_TOP.load(Ordering::SeqCst);
|
||||
let left = MONITOR_LEFT.load(Ordering::SeqCst);
|
||||
let right = MONITOR_RIGHT.load(Ordering::SeqCst);
|
||||
@@ -825,36 +961,38 @@ impl eframe::App for Komobar {
|
||||
bottom: monitor_size.bottom,
|
||||
right,
|
||||
};
|
||||
if *monitor_size != rect {
|
||||
if monitor_size != rect {
|
||||
tracing::info!(
|
||||
"Monitor coordinates/size has changed, storing new coordinates: {:#?}",
|
||||
monitor_size
|
||||
);
|
||||
|
||||
self.update_monitor_coordinates(monitor_size);
|
||||
self.update_monitor_coordinates(&monitor_size);
|
||||
|
||||
should_apply_config = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
|
||||
komorebi_notification_state
|
||||
.borrow_mut()
|
||||
.handle_notification(
|
||||
ctx,
|
||||
self.monitor_index,
|
||||
notification,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme.clone(),
|
||||
self.render_config.clone(),
|
||||
);
|
||||
if let Some(monitor_info) = &self.monitor_info {
|
||||
monitor_info.borrow_mut().update(
|
||||
self.monitor_index,
|
||||
notification.state,
|
||||
self.render_config.borrow().show_all_icons,
|
||||
);
|
||||
handle_notification(
|
||||
ctx,
|
||||
notification.event,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme.clone(),
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if should_apply_config {
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
|
||||
// Reposition the Bar
|
||||
self.position_bar();
|
||||
@@ -936,6 +1074,111 @@ impl eframe::App for Komobar {
|
||||
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
||||
|
||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||
if let Some(mouse_config) = &self.config.mouse {
|
||||
let command = if ui
|
||||
.input(|i| i.pointer.button_double_clicked(PointerButton::Primary))
|
||||
{
|
||||
tracing::debug!("Input: primary button double clicked");
|
||||
&mouse_config.on_primary_double_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Secondary)) {
|
||||
tracing::debug!("Input: secondary button clicked");
|
||||
&mouse_config.on_secondary_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Middle)) {
|
||||
tracing::debug!("Input: middle button clicked");
|
||||
&mouse_config.on_middle_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Extra1)) {
|
||||
tracing::debug!("Input: extra1 button clicked");
|
||||
&mouse_config.on_extra1_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Extra2)) {
|
||||
tracing::debug!("Input: extra2 button clicked");
|
||||
&mouse_config.on_extra2_click
|
||||
} else if self.input_config.act_on_vertical_scroll
|
||||
|| self.input_config.act_on_horizontal_scroll
|
||||
{
|
||||
let scroll_delta = ui.input(|input| input.smooth_scroll_delta);
|
||||
|
||||
self.input_config.accumulated_scroll_delta += scroll_delta;
|
||||
|
||||
if scroll_delta.y != 0.0 && self.input_config.act_on_vertical_scroll {
|
||||
// Do not store more than the max threshold
|
||||
self.input_config.accumulated_scroll_delta.y =
|
||||
self.input_config.accumulated_scroll_delta.y.clamp(
|
||||
-self.input_config.vertical_scroll_max_threshold,
|
||||
self.input_config.vertical_scroll_max_threshold,
|
||||
);
|
||||
|
||||
// When the accumulated scroll passes the threshold, trigger a tick.
|
||||
if self.input_config.accumulated_scroll_delta.y.abs()
|
||||
>= self.input_config.vertical_scroll_threshold
|
||||
{
|
||||
let direction_command =
|
||||
if self.input_config.accumulated_scroll_delta.y > 0.0 {
|
||||
&mouse_config.on_scroll_up
|
||||
} else {
|
||||
&mouse_config.on_scroll_down
|
||||
};
|
||||
|
||||
// Remove one tick's worth of scroll from the accumulator, preserving any excess.
|
||||
self.input_config.accumulated_scroll_delta.y -=
|
||||
self.input_config.vertical_scroll_threshold
|
||||
* self.input_config.accumulated_scroll_delta.y.signum();
|
||||
|
||||
tracing::debug!(
|
||||
"Input: vertical scroll ticked. excess: {} | threshold: {}",
|
||||
self.input_config.accumulated_scroll_delta.y,
|
||||
self.input_config.vertical_scroll_threshold
|
||||
);
|
||||
|
||||
direction_command
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
} else if scroll_delta.x != 0.0 && self.input_config.act_on_horizontal_scroll {
|
||||
// Do not store more than the max threshold
|
||||
self.input_config.accumulated_scroll_delta.x =
|
||||
self.input_config.accumulated_scroll_delta.x.clamp(
|
||||
-self.input_config.horizontal_scroll_max_threshold,
|
||||
self.input_config.horizontal_scroll_max_threshold,
|
||||
);
|
||||
|
||||
// When the accumulated scroll passes the threshold, trigger a tick.
|
||||
if self.input_config.accumulated_scroll_delta.x.abs()
|
||||
>= self.input_config.horizontal_scroll_threshold
|
||||
{
|
||||
let direction_command =
|
||||
if self.input_config.accumulated_scroll_delta.x > 0.0 {
|
||||
&mouse_config.on_scroll_left
|
||||
} else {
|
||||
&mouse_config.on_scroll_right
|
||||
};
|
||||
|
||||
// Remove one tick's worth of scroll from the accumulator, preserving any excess.
|
||||
self.input_config.accumulated_scroll_delta.x -=
|
||||
self.input_config.horizontal_scroll_threshold
|
||||
* self.input_config.accumulated_scroll_delta.x.signum();
|
||||
|
||||
tracing::debug!(
|
||||
"Input: horizontal scroll ticked. excess: {} | threshold: {}",
|
||||
self.input_config.accumulated_scroll_delta.x,
|
||||
self.input_config.horizontal_scroll_threshold
|
||||
);
|
||||
|
||||
direction_command
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
|
||||
if let Some(command) = command {
|
||||
command.execute(self.mouse_follows_focus);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply grouping logic for the bar as a whole
|
||||
let area_frame = if let Some(frame) = &self.config.frame {
|
||||
Frame::NONE
|
||||
@@ -1075,3 +1318,66 @@ pub enum Alignment {
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn handle_notification(
|
||||
ctx: &Context,
|
||||
event: komorebi_client::NotificationEvent,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
bg_color_with_alpha: Rc<RefCell<Color32>>,
|
||||
transparency_alpha: Option<u8>,
|
||||
grouping: Option<Grouping>,
|
||||
default_theme: Option<KomobarTheme>,
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
if let NotificationEvent::Socket(message) = event {
|
||||
match message {
|
||||
SocketMessage::ReloadStaticConfiguration(path) => {
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
} else if let Some(default_theme) = default_theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
default_theme,
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!(
|
||||
"removed theme from updated komorebi.json and applied default theme"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"theme was removed from updated komorebi.json but there was no default theme to apply"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(*theme),
|
||||
bg_color,
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::DEFAULT_PADDING;
|
||||
use crate::bar::exec_powershell;
|
||||
use crate::render::Grouping;
|
||||
use crate::widgets::widget::WidgetConfig;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -13,7 +16,7 @@ use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.36`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.40`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar height (default: 50)
|
||||
pub height: Option<f32>,
|
||||
@@ -90,6 +93,8 @@ pub struct KomobarConfig {
|
||||
pub widget_spacing: Option<f32>,
|
||||
/// Visual grouping for widgets
|
||||
pub grouping: Option<Grouping>,
|
||||
/// Options for mouse interaction on the bar
|
||||
pub mouse: Option<MouseConfig>,
|
||||
/// Left side widgets (ordered left-to-right)
|
||||
pub left_widgets: Vec<WidgetConfig>,
|
||||
/// Center widgets (ordered left-to-right)
|
||||
@@ -115,7 +120,9 @@ impl KomobarConfig {
|
||||
}
|
||||
|
||||
if display {
|
||||
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
|
||||
println!(
|
||||
"\nYour bar configuration file contains some options that have been renamed or deprecated:\n"
|
||||
);
|
||||
for (canonical, aliases) in map {
|
||||
for alias in aliases {
|
||||
if raw.contains(alias) {
|
||||
@@ -325,6 +332,146 @@ pub fn get_individual_spacing(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum MouseMessage {
|
||||
/// Send a message to the komorebi client.
|
||||
/// By default, a batch of messages are sent in the following order:
|
||||
/// FocusMonitorAtCursor =>
|
||||
/// MouseFollowsFocus(false) =>
|
||||
/// {message} =>
|
||||
/// MouseFollowsFocus({original.value})
|
||||
///
|
||||
/// Example:
|
||||
/// ```json
|
||||
/// "on_extra2_click": {
|
||||
/// "message": {
|
||||
/// "type": "NewWorkspace"
|
||||
/// }
|
||||
/// },
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "on_middle_click": {
|
||||
/// "focus_monitor_at_cursor": false,
|
||||
/// "ignore_mouse_follows_focus": false,
|
||||
/// "message": {
|
||||
/// "type": "TogglePause"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "on_scroll_up": {
|
||||
/// "message": {
|
||||
/// "type": "CycleFocusWorkspace",
|
||||
/// "content": "Previous"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
Komorebi(KomorebiMouseMessage),
|
||||
/// Execute a custom command.
|
||||
/// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved.
|
||||
/// Example: `komorebic toggle-pause`
|
||||
Command(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiMouseMessage {
|
||||
/// Send the FocusMonitorAtCursor message (default:true)
|
||||
pub focus_monitor_at_cursor: Option<bool>,
|
||||
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message (default:true)
|
||||
pub ignore_mouse_follows_focus: Option<bool>,
|
||||
/// The message to send to the komorebi client
|
||||
pub message: komorebi_client::SocketMessage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct MouseConfig {
|
||||
/// Command to send on primary/left double button click
|
||||
pub on_primary_double_click: Option<MouseMessage>,
|
||||
/// Command to send on secondary/right button click
|
||||
pub on_secondary_click: Option<MouseMessage>,
|
||||
/// Command to send on middle button click
|
||||
pub on_middle_click: Option<MouseMessage>,
|
||||
/// Command to send on extra1/back button click
|
||||
pub on_extra1_click: Option<MouseMessage>,
|
||||
/// Command to send on extra2/forward button click
|
||||
pub on_extra2_click: Option<MouseMessage>,
|
||||
|
||||
/// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
||||
pub vertical_scroll_threshold: Option<f32>,
|
||||
/// Command to send on scrolling up (every tick)
|
||||
pub on_scroll_up: Option<MouseMessage>,
|
||||
/// Command to send on scrolling down (every tick)
|
||||
pub on_scroll_down: Option<MouseMessage>,
|
||||
|
||||
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
||||
pub horizontal_scroll_threshold: Option<f32>,
|
||||
/// Command to send on scrolling left (every tick)
|
||||
pub on_scroll_left: Option<MouseMessage>,
|
||||
/// Command to send on scrolling right (every tick)
|
||||
pub on_scroll_right: Option<MouseMessage>,
|
||||
}
|
||||
|
||||
impl MouseConfig {
|
||||
pub fn has_command(&self) -> bool {
|
||||
[
|
||||
&self.on_primary_double_click,
|
||||
&self.on_secondary_click,
|
||||
&self.on_middle_click,
|
||||
&self.on_extra1_click,
|
||||
&self.on_extra2_click,
|
||||
&self.on_scroll_up,
|
||||
&self.on_scroll_down,
|
||||
&self.on_scroll_left,
|
||||
&self.on_scroll_right,
|
||||
]
|
||||
.iter()
|
||||
.any(|opt| matches!(opt, Some(MouseMessage::Command(_))))
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseMessage {
|
||||
pub fn execute(&self, mouse_follows_focus: bool) {
|
||||
match self {
|
||||
MouseMessage::Komorebi(config) => {
|
||||
let mut messages = Vec::new();
|
||||
|
||||
if config.focus_monitor_at_cursor.unwrap_or(true) {
|
||||
messages.push(SocketMessage::FocusMonitorAtCursor);
|
||||
}
|
||||
|
||||
if config.ignore_mouse_follows_focus.unwrap_or(true) {
|
||||
messages.push(SocketMessage::MouseFollowsFocus(false));
|
||||
messages.push(config.message.clone());
|
||||
messages.push(SocketMessage::MouseFollowsFocus(mouse_follows_focus));
|
||||
} else {
|
||||
messages.push(config.message.clone());
|
||||
}
|
||||
|
||||
tracing::debug!("Sending messages: {messages:?}");
|
||||
|
||||
if komorebi_client::send_batch(messages).is_err() {
|
||||
tracing::error!("could not send commands");
|
||||
}
|
||||
}
|
||||
MouseMessage::Command(cmd) => {
|
||||
tracing::debug!("Executing command: {}", cmd);
|
||||
|
||||
let cmd_no_env = cmd.replace_env();
|
||||
|
||||
if exec_powershell(cmd_no_env.to_str().expect("Invalid command")).is_err() {
|
||||
tracing::error!("Failed to execute '{}'", cmd);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
|
||||
@@ -15,12 +15,10 @@ use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::replace_env_in_path;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::SubscribeOptions;
|
||||
use std::collections::HashMap;
|
||||
use komorebi_client::replace_env_in_path;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
@@ -28,16 +26,14 @@ use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||
use windows::Win32::System::Threading::GetCurrentThreadId;
|
||||
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
|
||||
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
use windows_core::BOOL;
|
||||
@@ -53,9 +49,6 @@ pub static DEFAULT_PADDING: f32 = 10.0;
|
||||
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
struct Opts {
|
||||
@@ -110,7 +103,7 @@ fn process_hwnd() -> Option<isize> {
|
||||
}
|
||||
|
||||
pub enum KomorebiEvent {
|
||||
Notification(komorebi_client::Notification),
|
||||
Notification(Box<komorebi_client::Notification>),
|
||||
Reconnect,
|
||||
}
|
||||
|
||||
@@ -121,14 +114,14 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
if opts.schema {
|
||||
let settings = schemars::gen::SchemaSettings::default().with(|s| {
|
||||
let settings = schemars::r#gen::SchemaSettings::default().with(|s| {
|
||||
s.option_nullable = false;
|
||||
s.option_add_null_type = false;
|
||||
s.inline_subschemas = true;
|
||||
});
|
||||
|
||||
let gen = settings.into_generator();
|
||||
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
|
||||
let generator = settings.into_generator();
|
||||
let socket_message = generator.into_root_schema_for::<KomobarConfig>();
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
|
||||
println!("{schema}");
|
||||
@@ -144,13 +137,17 @@ fn main() -> color_eyre::Result<()> {
|
||||
}
|
||||
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
}
|
||||
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
}
|
||||
|
||||
tracing::subscriber::set_global_default(
|
||||
@@ -166,8 +163,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
home
|
||||
@@ -238,17 +234,17 @@ fn main() -> color_eyre::Result<()> {
|
||||
.map_or(usr_monitor_index, |i| *i);
|
||||
|
||||
MONITOR_RIGHT.store(
|
||||
state.monitors.elements()[monitor_index].size().right,
|
||||
state.monitors.elements()[monitor_index].size.right,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[monitor_index].size().top,
|
||||
state.monitors.elements()[monitor_index].size.top,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_LEFT.store(
|
||||
state.monitors.elements()[monitor_index].size().left,
|
||||
state.monitors.elements()[monitor_index].size.left,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
@@ -258,11 +254,11 @@ fn main() -> color_eyre::Result<()> {
|
||||
None => {
|
||||
config.position = Some(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
||||
x: state.monitors.elements()[monitor_index].size.left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size.top as f32,
|
||||
}),
|
||||
end: Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size().right as f32,
|
||||
x: state.monitors.elements()[monitor_index].size.right as f32,
|
||||
y: 50.0,
|
||||
}),
|
||||
})
|
||||
@@ -270,14 +266,14 @@ fn main() -> color_eyre::Result<()> {
|
||||
Some(ref mut position) => {
|
||||
if position.start.is_none() {
|
||||
position.start = Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
||||
x: state.monitors.elements()[monitor_index].size.left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size.top as f32,
|
||||
});
|
||||
}
|
||||
|
||||
if position.end.is_none() {
|
||||
position.end = Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size().right as f32,
|
||||
x: state.monitors.elements()[monitor_index].size.right as f32,
|
||||
y: 50.0,
|
||||
})
|
||||
}
|
||||
@@ -362,7 +358,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
while komorebi_client::send_message(
|
||||
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
|
||||
)
|
||||
.is_err()
|
||||
.is_err()
|
||||
{
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
@@ -385,7 +381,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
Ok(notification) => {
|
||||
tracing::debug!("received notification from komorebi");
|
||||
|
||||
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
|
||||
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(Box::new(notification))) {
|
||||
tracing::error!("could not send komorebi notification update to gui thread: {error}")
|
||||
}
|
||||
|
||||
@@ -413,5 +409,5 @@ fn main() -> color_eyre::Result<()> {
|
||||
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
|
||||
}),
|
||||
)
|
||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
@@ -18,9 +18,9 @@ use komorebi_client::Rgb;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::num::NonZeroU32;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
|
||||
406
komorebi-bar/src/widgets/applications.rs
Normal file
406
komorebi-bar/src/widgets/applications.rs
Normal file
@@ -0,0 +1,406 @@
|
||||
use super::ImageIcon;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Image;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::RichText;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::vec2;
|
||||
use komorebi_client::PathExt;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tracing;
|
||||
use which::which;
|
||||
|
||||
/// Minimum interval between consecutive application launches to prevent accidental spamming.
|
||||
const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ApplicationsConfig {
|
||||
/// Enables or disables the applications widget.
|
||||
pub enable: bool,
|
||||
/// Whether to show the launch command on hover (optional).
|
||||
/// Could be overridden per application. Defaults to `false` if not set.
|
||||
pub show_command_on_hover: Option<bool>,
|
||||
/// Horizontal spacing between application buttons.
|
||||
pub spacing: Option<f32>,
|
||||
/// Default display format for all applications (optional).
|
||||
/// Could be overridden per application. Defaults to `Icon`.
|
||||
pub display: Option<DisplayFormat>,
|
||||
/// List of configured applications to display.
|
||||
pub items: Vec<AppConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct AppConfig {
|
||||
/// Whether to enable this application button (optional).
|
||||
/// Inherits from the global `Applications` setting if omitted.
|
||||
pub enable: Option<bool>,
|
||||
/// Whether to show the launch command on hover (optional).
|
||||
/// Inherits from the global `Applications` setting if omitted.
|
||||
pub show_command_on_hover: Option<bool>,
|
||||
/// Display name of the application.
|
||||
pub name: String,
|
||||
/// Optional icon: a path to an image or a text-based glyph (e.g., from Nerd Fonts).
|
||||
/// If not set, and if the `command` is a path to an executable, an icon might be extracted from it.
|
||||
/// Note: glyphs require a compatible `font_family`.
|
||||
pub icon: Option<String>,
|
||||
/// Command to execute (e.g. path to the application or shell command).
|
||||
pub command: String,
|
||||
/// Display format for this application button (optional). Overrides global format if set.
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum DisplayFormat {
|
||||
/// Show only the application icon.
|
||||
#[default]
|
||||
Icon,
|
||||
/// Show only the application name as text.
|
||||
Text,
|
||||
/// Show both the application icon and name.
|
||||
IconAndText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Applications {
|
||||
/// Whether the applications widget is enabled.
|
||||
pub enable: bool,
|
||||
/// Horizontal spacing between application buttons.
|
||||
pub spacing: Option<f32>,
|
||||
/// Applications to be rendered in the UI.
|
||||
pub items: Vec<App>,
|
||||
}
|
||||
|
||||
impl BarWidget for Applications {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if !self.enable {
|
||||
return;
|
||||
}
|
||||
|
||||
let icon_config = IconConfig {
|
||||
font_id: config.icon_font_id.clone(),
|
||||
size: config.icon_font_id.size,
|
||||
color: ctx.style().visuals.selection.stroke.color,
|
||||
};
|
||||
|
||||
if let Some(spacing) = self.spacing {
|
||||
ui.spacing_mut().item_spacing.x = spacing;
|
||||
}
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for app in &mut self.items {
|
||||
app.render(ctx, ui, &icon_config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ApplicationsConfig> for Applications {
|
||||
fn from(applications_config: &ApplicationsConfig) -> Self {
|
||||
let items = applications_config
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, config)| {
|
||||
let command = UserCommand::new(&config.command);
|
||||
|
||||
App {
|
||||
enable: config.enable.unwrap_or(applications_config.enable),
|
||||
#[allow(clippy::obfuscated_if_else)]
|
||||
name: config
|
||||
.name
|
||||
.is_empty()
|
||||
.then(|| format!("App {}", index + 1))
|
||||
.unwrap_or_else(|| config.name.clone()),
|
||||
icon: Icon::try_from_path(config.icon.as_deref())
|
||||
.or_else(|| Icon::try_from_command(&command)),
|
||||
command,
|
||||
display: config
|
||||
.display
|
||||
.or(applications_config.display)
|
||||
.unwrap_or_default(),
|
||||
show_command_on_hover: config
|
||||
.show_command_on_hover
|
||||
.or(applications_config.show_command_on_hover)
|
||||
.unwrap_or(false),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
enable: applications_config.enable,
|
||||
items,
|
||||
spacing: applications_config.spacing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single resolved application entry used at runtime.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct App {
|
||||
/// Whether this application is enabled.
|
||||
pub enable: bool,
|
||||
/// Display name of the application. Defaults to "App N" if not set.
|
||||
pub name: String,
|
||||
/// Icon to display for this application, if available.
|
||||
pub icon: Option<Icon>,
|
||||
/// Command to execute when the application is launched.
|
||||
pub command: UserCommand,
|
||||
/// Display format (icon, text, or both).
|
||||
pub display: DisplayFormat,
|
||||
/// Whether to show the launch command on hover.
|
||||
pub show_command_on_hover: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Renders the application button in the provided `Ui` context with a given icon size.
|
||||
#[inline]
|
||||
pub fn render(&mut self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||
if self.enable
|
||||
&& SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = Vec2::splat(4.0);
|
||||
|
||||
match self.display {
|
||||
DisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
|
||||
DisplayFormat::Text => self.draw_name(ui),
|
||||
DisplayFormat::IconAndText => {
|
||||
self.draw_icon(ctx, ui, icon_config);
|
||||
self.draw_name(ui);
|
||||
}
|
||||
}
|
||||
|
||||
// Add hover text with command information
|
||||
let response = ui.response();
|
||||
if self.show_command_on_hover {
|
||||
response.on_hover_text(format!("Launch: {}", self.command.as_ref()));
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
// Launch the application when clicked
|
||||
self.command.launch_if_ready();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the application's icon within the UI if available,
|
||||
/// or falls back to a default placeholder icon.
|
||||
#[inline]
|
||||
fn draw_icon(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||
if let Some(icon) = &self.icon {
|
||||
icon.draw(ctx, ui, icon_config);
|
||||
} else {
|
||||
Icon::draw_fallback(ui, Vec2::splat(icon_config.size));
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays the application's name as a non-selectable label within the UI.
|
||||
#[inline]
|
||||
fn draw_name(&self, ui: &mut Ui) {
|
||||
ui.add(Label::new(&self.name).selectable(false));
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds image/text data to be used as an icon in the UI.
|
||||
/// This represents source icon data before rendering.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Icon {
|
||||
/// RGBA image used for rendering the icon.
|
||||
Image(ImageIcon),
|
||||
/// Text-based icon, e.g. from a font like Nerd Fonts.
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
/// Attempts to create an [`Icon`] from a string path or text glyph/glyphs.
|
||||
///
|
||||
/// - Environment variables in the path are resolved using [`PathExt::replace_env`].
|
||||
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved path.
|
||||
/// - If the path is invalid but the string is non-empty, it is interpreted as a text-based icon and
|
||||
/// returned as [`Icon::Text`].
|
||||
/// - Returns `None` if the input is empty, `None`, or image loading fails.
|
||||
#[inline]
|
||||
pub fn try_from_path(icon: Option<&str>) -> Option<Self> {
|
||||
let icon = icon.map(str::trim)?;
|
||||
if icon.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = icon.replace_env();
|
||||
if !path.is_file() {
|
||||
return Some(Icon::Text(icon.to_owned()));
|
||||
}
|
||||
|
||||
let image_icon = ImageIcon::try_load(path.as_ref(), || match image::open(&path) {
|
||||
Ok(img) => Some(img),
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to load icon from {:?}, error: {}", path, err);
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
Some(Icon::Image(image_icon))
|
||||
}
|
||||
|
||||
/// Attempts to create an [`Icon`] by extracting an image from the executable path of a [`UserCommand`].
|
||||
///
|
||||
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved executable path.
|
||||
/// - Returns [`Icon::Image`] if an icon is successfully extracted.
|
||||
/// - Returns `None` if the executable path is unavailable or icon extraction fails.
|
||||
#[inline]
|
||||
pub fn try_from_command(command: &UserCommand) -> Option<Self> {
|
||||
let path = command.get_executable()?;
|
||||
let image_icon = ImageIcon::try_load(path.as_ref(), || {
|
||||
let path_str = path.to_str()?;
|
||||
windows_icons::get_icon_by_path(path_str)
|
||||
.or_else(|| windows_icons_fallback::get_icon_by_path(path_str))
|
||||
})?;
|
||||
Some(Icon::Image(image_icon))
|
||||
}
|
||||
|
||||
/// Renders the icon in the given [`Ui`] using the provided [`IconConfig`].
|
||||
#[inline]
|
||||
pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||
match self {
|
||||
Icon::Image(image_icon) => {
|
||||
Frame::NONE
|
||||
.inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Image::from_texture(&image_icon.texture(ctx))
|
||||
.maintain_aspect_ratio(true)
|
||||
.fit_to_exact_size(Vec2::splat(icon_config.size)),
|
||||
);
|
||||
});
|
||||
}
|
||||
Icon::Text(icon) => {
|
||||
let rich_text = RichText::new(icon)
|
||||
.font(icon_config.font_id.clone())
|
||||
.size(icon_config.size)
|
||||
.color(icon_config.color);
|
||||
ui.add(Label::new(rich_text).selectable(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a fallback icon when the specified icon cannot be loaded.
|
||||
/// Displays a simple crossed-out rectangle as a placeholder.
|
||||
#[inline]
|
||||
pub fn draw_fallback(ui: &mut Ui, icon_size: Vec2) {
|
||||
let (response, painter) = ui.allocate_painter(icon_size, Sense::hover());
|
||||
let stroke = Stroke::new(1.0, ui.style().visuals.text_color());
|
||||
let mut rect = response.rect;
|
||||
let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
|
||||
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration structure for icon rendering
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IconConfig {
|
||||
/// Font used for text-based icons
|
||||
pub font_id: FontId,
|
||||
/// Size of the icon
|
||||
pub size: f32,
|
||||
/// Color of the icon used for text-based icons
|
||||
pub color: Color32,
|
||||
}
|
||||
|
||||
/// A structure to manage command execution with cooldown prevention.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserCommand {
|
||||
/// The command string to execute
|
||||
pub command: Arc<str>,
|
||||
/// Last time this command was executed (used for cooldown control)
|
||||
pub last_launch: Instant,
|
||||
}
|
||||
|
||||
impl AsRef<str> for UserCommand {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.command
|
||||
}
|
||||
}
|
||||
|
||||
impl UserCommand {
|
||||
/// Creates a new [`UserCommand`] with environment variables in the command path
|
||||
/// resolved using [`PathExt::replace_env`].
|
||||
#[inline]
|
||||
pub fn new(command: &str) -> Self {
|
||||
// Allow immediate launch by initializing last_launch in the past
|
||||
let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL;
|
||||
|
||||
Self {
|
||||
command: Arc::from(command.replace_env().to_str().unwrap_or_default()),
|
||||
last_launch,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to resolve the executable path from the command string.
|
||||
///
|
||||
/// Resolution logic:
|
||||
/// - Splits the command by ".exe" and checks if the first part is an existing file.
|
||||
/// - If not, attempts to locate the binary using [`which`] on this name.
|
||||
/// - If still unresolved, takes the first word (separated by whitespace) and attempts
|
||||
/// to find it in the system `PATH` using [`which`].
|
||||
///
|
||||
/// Returns `None` if no executable path can be determined.
|
||||
#[inline]
|
||||
pub fn get_executable(&self) -> Option<Cow<'_, Path>> {
|
||||
if let Some(binary) = self.command.split(".exe").next().map(Path::new) {
|
||||
if binary.is_file() {
|
||||
return Some(Cow::Borrowed(binary));
|
||||
} else if let Ok(binary) = which(binary) {
|
||||
return Some(Cow::Owned(binary));
|
||||
}
|
||||
}
|
||||
|
||||
which(self.command.split(' ').next()?).ok().map(Cow::Owned)
|
||||
}
|
||||
|
||||
/// Attempts to launch the specified command in a separate thread if enough time has passed
|
||||
/// since the last launch. This prevents repeated launches from rapid consecutive clicks.
|
||||
///
|
||||
/// Errors during launch are logged using the `tracing` crate.
|
||||
pub fn launch_if_ready(&mut self) {
|
||||
let now = Instant::now();
|
||||
// Check if enough time has passed since the last launch
|
||||
if now.duration_since(self.last_launch) < MIN_LAUNCH_INTERVAL {
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_launch = now;
|
||||
let command_string = self.command.clone();
|
||||
// Launch the application in a separate thread to avoid blocking the UI
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = Command::new("cmd").args(["/C", &command_string]).spawn() {
|
||||
tracing::error!("Failed to launch command '{}': {}", command_string, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,17 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use starship_battery::units::ratio::percent;
|
||||
use starship_battery::Manager;
|
||||
use starship_battery::State;
|
||||
use starship_battery::units::ratio::percent;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
@@ -87,41 +87,41 @@ impl Battery {
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
output = None;
|
||||
|
||||
if let Ok(mut batteries) = self.manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>().round() as u8;
|
||||
if let Ok(mut batteries) = self.manager.batteries()
|
||||
&& let Some(Ok(first)) = batteries.nth(0)
|
||||
{
|
||||
let percentage = first.state_of_charge().get::<percent>().round() as u8;
|
||||
|
||||
if percentage == 100 && self.hide_on_full_charge {
|
||||
output = None
|
||||
} else {
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => {
|
||||
self.state = match percentage {
|
||||
p if p > 75 => BatteryState::Discharging,
|
||||
p if p > 50 => BatteryState::High,
|
||||
p if p > 25 => BatteryState::Medium,
|
||||
p if p > 10 => BatteryState::Low,
|
||||
_ => BatteryState::Warning,
|
||||
}
|
||||
if percentage == 100 && self.hide_on_full_charge {
|
||||
output = None
|
||||
} else {
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => {
|
||||
self.state = match percentage {
|
||||
p if p > 75 => BatteryState::Discharging,
|
||||
p if p > 50 => BatteryState::High,
|
||||
p if p > 25 => BatteryState::Medium,
|
||||
p if p > 10 => BatteryState::Low,
|
||||
_ => BatteryState::Warning,
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
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 +176,11 @@ impl BarWidget for Battery {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
&& let Err(error) = Command::new("cmd.exe")
|
||||
.args(["/C", "start", "ms-settings:batterysaver"])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
{
|
||||
eprintln!("{error}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -76,8 +76,8 @@ impl Cpu {
|
||||
|
||||
CpuOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {used}%"),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{used}%"),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
@@ -120,12 +120,10 @@ impl BarWidget for Cpu {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) =
|
||||
&& let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
{
|
||||
eprintln!("{error}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@ use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -166,7 +167,7 @@ impl Date {
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
Err(_) => format!("Invalid timezone: {}", timezone),
|
||||
Err(_) => format!("Invalid timezone: {timezone}"),
|
||||
},
|
||||
None => Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
@@ -225,7 +226,7 @@ impl BarWidget for Date {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
|
||||
.selectable(false),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use color_eyre::eyre;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows::Win32::Globalization::LCIDToLocaleName;
|
||||
@@ -80,7 +82,7 @@ pub struct Keyboard {
|
||||
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
|
||||
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
|
||||
/// invalid UTF-16 characters during conversion.
|
||||
fn get_active_keyboard_layout() -> Result<String, ()> {
|
||||
fn get_active_keyboard_layout() -> eyre::Result<String, ()> {
|
||||
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
|
||||
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
|
||||
|
||||
@@ -169,7 +171,10 @@ impl BarWidget for Keyboard {
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
|
||||
ui.add(
|
||||
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
|
||||
.selectable(false),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ use crate::config::DisplayFormat;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::komorebi::KomorebiLayoutConfig;
|
||||
use eframe::egui::vec2;
|
||||
use color_eyre::eyre;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
@@ -13,11 +13,12 @@ use eframe::egui::Stroke;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::vec2;
|
||||
use komorebi_client::SocketMessage;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::de::Error;
|
||||
use serde_json::from_str;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
@@ -34,15 +35,14 @@ pub enum 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
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: String = String::deserialize(deserializer)?;
|
||||
|
||||
// Attempt to deserialize the string as a DefaultLayout
|
||||
if let Ok(default_layout) =
|
||||
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
|
||||
if let Ok(default_layout) = from_str::<komorebi_client::DefaultLayout>(&format!("\"{s}\""))
|
||||
{
|
||||
return Ok(KomorebiLayout::Default(default_layout));
|
||||
}
|
||||
@@ -53,7 +53,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
"Floating" => Ok(KomorebiLayout::Floating),
|
||||
"Paused" => Ok(KomorebiLayout::Paused),
|
||||
"Custom" => Ok(KomorebiLayout::Custom),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,16 +92,15 @@ impl KomorebiLayout {
|
||||
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
|
||||
match self {
|
||||
KomorebiLayout::Default(option) => {
|
||||
if let Some(ws_idx) = workspace_idx {
|
||||
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
||||
if let Some(ws_idx) = workspace_idx
|
||||
&& komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
||||
monitor_idx,
|
||||
ws_idx,
|
||||
*option,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
||||
}
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Monocle => {
|
||||
@@ -188,6 +187,12 @@ impl KomorebiLayout {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
// TODO: @CtByte can you think of a nice icon to draw here?
|
||||
komorebi_client::DefaultLayout::Scrolling => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
},
|
||||
KomorebiLayout::Monocle => {}
|
||||
KomorebiLayout::Floating => {
|
||||
@@ -264,57 +269,53 @@ impl KomorebiLayout {
|
||||
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
|
||||
}
|
||||
|
||||
if show_options {
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
Frame::NONE.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||
.selectable(false),
|
||||
);
|
||||
if show_options && let Some(workspace_idx) = workspace_idx {
|
||||
Frame::NONE.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||
.selectable(false),
|
||||
);
|
||||
|
||||
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::HorizontalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
||||
//KomorebiLayout::Custom,
|
||||
KomorebiLayout::Monocle,
|
||||
KomorebiLayout::Floating,
|
||||
KomorebiLayout::Paused,
|
||||
]);
|
||||
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::HorizontalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
||||
//KomorebiLayout::Custom,
|
||||
KomorebiLayout::Monocle,
|
||||
KomorebiLayout::Floating,
|
||||
KomorebiLayout::Paused,
|
||||
]);
|
||||
|
||||
for layout_option in &mut layout_options {
|
||||
let is_selected = self == layout_option;
|
||||
for layout_option in &mut layout_options {
|
||||
let is_selected = self == layout_option;
|
||||
|
||||
if SelectableFrame::new(is_selected)
|
||||
.show(ui, |ui| {
|
||||
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
|
||||
})
|
||||
.on_hover_text(match layout_option {
|
||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
||||
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
||||
KomorebiLayout::Custom => "Custom".to_string(),
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
||||
show_options = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
if SelectableFrame::new(is_selected)
|
||||
.show(ui, |ui| {
|
||||
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
|
||||
})
|
||||
.on_hover_text(match layout_option {
|
||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
||||
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
||||
KomorebiLayout::Custom => "Custom".to_string(),
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
||||
show_options = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -40,36 +40,34 @@ impl Media {
|
||||
enable,
|
||||
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
|
||||
.unwrap()
|
||||
.get()
|
||||
.join()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
||||
if let Ok(op) = session.TryTogglePlayPauseAsync() {
|
||||
op.get().unwrap_or_default();
|
||||
}
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(op) = session.TryTogglePlayPauseAsync()
|
||||
{
|
||||
op.join().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
fn output(&mut self) -> String {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
||||
if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
|
||||
if let Ok(properties) = operation.get() {
|
||||
if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
|
||||
if artist.is_empty() {
|
||||
return format!("{title}");
|
||||
}
|
||||
|
||||
if title.is_empty() {
|
||||
return format!("{artist}");
|
||||
}
|
||||
|
||||
return format!("{artist} - {title}");
|
||||
}
|
||||
}
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
|
||||
&& let Ok(properties) = operation.join()
|
||||
&& let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title())
|
||||
{
|
||||
if artist.is_empty() {
|
||||
return format!("{title}");
|
||||
}
|
||||
|
||||
if title.is_empty() {
|
||||
return format!("{artist}");
|
||||
}
|
||||
|
||||
return format!("{artist} - {title}");
|
||||
}
|
||||
|
||||
String::new()
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -79,9 +79,9 @@ impl Memory {
|
||||
MemoryOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", usage)
|
||||
format!("RAM: {usage}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{usage}%"),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
@@ -124,12 +124,10 @@ impl BarWidget for Memory {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) =
|
||||
&& let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
{
|
||||
eprintln!("{error}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
use eframe::egui::ColorImage;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::TextureHandle;
|
||||
use eframe::egui::TextureOptions;
|
||||
use image::RgbaImage;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::RwLock;
|
||||
|
||||
pub mod applications;
|
||||
pub mod battery;
|
||||
pub mod cpu;
|
||||
pub mod date;
|
||||
@@ -11,3 +23,151 @@ pub mod storage;
|
||||
pub mod time;
|
||||
pub mod update;
|
||||
pub mod widget;
|
||||
|
||||
/// Global cache for icon images and their associated GPU textures.
|
||||
pub static ICONS_CACHE: IconsCache = IconsCache::new();
|
||||
|
||||
/// In-memory cache for icon images and their associated GPU textures.
|
||||
///
|
||||
/// Stores raw [`ColorImage`]s and [`TextureHandle`]s keyed by [`ImageIconId`].
|
||||
/// Texture entries are context-dependent and automatically invalidated when the [`Context`] changes.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct IconsCache {
|
||||
textures: LazyLock<RwLock<(Option<Context>, HashMap<ImageIconId, TextureHandle>)>>,
|
||||
images: LazyLock<RwLock<HashMap<ImageIconId, Arc<ColorImage>>>>,
|
||||
}
|
||||
|
||||
impl IconsCache {
|
||||
/// Creates a new empty IconsCache instance.
|
||||
#[inline]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
textures: LazyLock::new(|| RwLock::new((None, HashMap::new()))),
|
||||
images: LazyLock::new(|| RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves or creates a texture handle for the given icon ID and image.
|
||||
///
|
||||
/// If a texture for the given ID already exists for the current [`Context`], it is reused.
|
||||
/// Otherwise, a new texture is created, inserted into the cache, and returned.
|
||||
/// The cache is reset if the [`Context`] has changed.
|
||||
#[inline]
|
||||
pub fn texture(&self, ctx: &Context, id: &ImageIconId, img: &Arc<ColorImage>) -> TextureHandle {
|
||||
if let Some(texture) = self.get_texture(ctx, id) {
|
||||
return texture;
|
||||
}
|
||||
let texture_handle = ctx.load_texture("icon", img.clone(), TextureOptions::default());
|
||||
self.insert_texture(ctx, id.clone(), texture_handle.clone());
|
||||
texture_handle
|
||||
}
|
||||
|
||||
/// Returns the cached texture for the given icon ID if it exists and matches the current [`Context`].
|
||||
pub fn get_texture(&self, ctx: &Context, id: &ImageIconId) -> Option<TextureHandle> {
|
||||
let textures_lock = self.textures.read().unwrap();
|
||||
if textures_lock.0.as_ref() == Some(ctx) {
|
||||
return textures_lock.1.get(id).cloned();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Inserts a texture handle, resetting the cache if the [`Context`] has changed.
|
||||
pub fn insert_texture(&self, ctx: &Context, id: ImageIconId, texture: TextureHandle) {
|
||||
let mut textures_lock = self.textures.write().unwrap();
|
||||
|
||||
if textures_lock.0.as_ref() != Some(ctx) {
|
||||
textures_lock.0 = Some(ctx.clone());
|
||||
textures_lock.1.clear();
|
||||
}
|
||||
|
||||
textures_lock.1.insert(id, texture);
|
||||
}
|
||||
|
||||
/// Returns the cached image for the given icon ID, if available.
|
||||
pub fn get_image(&self, id: &ImageIconId) -> Option<Arc<ColorImage>> {
|
||||
self.images.read().unwrap().get(id).cloned()
|
||||
}
|
||||
|
||||
/// Caches a raw [`ColorImage`] associated with the given icon ID.
|
||||
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
|
||||
self.images.write().unwrap().insert(id, image);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
|
||||
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||
let pixels = rgba_image.as_flat_samples();
|
||||
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
|
||||
}
|
||||
|
||||
/// Represents an image-based icon with a unique ID and pixel data.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImageIcon {
|
||||
/// Unique identifier for the image icon, used for texture caching.
|
||||
pub id: ImageIconId,
|
||||
/// Shared pixel data of the icon in `ColorImage` format.
|
||||
pub image: Arc<ColorImage>,
|
||||
}
|
||||
|
||||
impl ImageIcon {
|
||||
/// Creates a new [`ImageIcon`] from the given ID and image data.
|
||||
#[inline]
|
||||
pub fn new(id: ImageIconId, image: Arc<ColorImage>) -> Self {
|
||||
Self { id, image }
|
||||
}
|
||||
|
||||
/// Loads an [`ImageIcon`] from [`ICONS_CACHE`] or calls `loader` if not cached.
|
||||
/// The loaded image is converted to a [`ColorImage`], cached, and returned.
|
||||
#[inline]
|
||||
pub fn try_load<F, I>(id: impl Into<ImageIconId>, loader: F) -> Option<Self>
|
||||
where
|
||||
F: FnOnce() -> Option<I>,
|
||||
I: Into<RgbaImage>,
|
||||
{
|
||||
let id = id.into();
|
||||
let image = ICONS_CACHE.get_image(&id).or_else(|| {
|
||||
let img = loader()?;
|
||||
let img = Arc::new(rgba_to_color_image(&img.into()));
|
||||
ICONS_CACHE.insert_image(id.clone(), img.clone());
|
||||
Some(img)
|
||||
})?;
|
||||
|
||||
Some(ImageIcon::new(id, image))
|
||||
}
|
||||
|
||||
/// Returns a texture handle for the icon, using the given [`Context`].
|
||||
///
|
||||
/// If the texture is already cached in [`ICONS_CACHE`], it is reused.
|
||||
/// Otherwise, a new texture is created from the [`ColorImage`] and cached.
|
||||
#[inline]
|
||||
pub fn texture(&self, ctx: &Context) -> TextureHandle {
|
||||
ICONS_CACHE.texture(ctx, &self.id, &self.image)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unique identifier for an image-based icon.
|
||||
///
|
||||
/// Used to distinguish cached images and textures by either a file path
|
||||
/// or a Windows window handle.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum ImageIconId {
|
||||
/// Identifier based on a file system path.
|
||||
Path(Arc<Path>),
|
||||
/// Windows HWND handle.
|
||||
Hwnd(isize),
|
||||
}
|
||||
|
||||
impl From<&Path> for ImageIconId {
|
||||
#[inline]
|
||||
fn from(value: &Path) -> Self {
|
||||
Self::Path(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<isize> for ImageIconId {
|
||||
#[inline]
|
||||
fn from(value: isize) -> Self {
|
||||
Self::Hwnd(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,23 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use num_derive::FromPrimitive;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Networks;
|
||||
@@ -58,6 +63,7 @@ pub struct NetworkSelectConfig {
|
||||
|
||||
impl From<NetworkConfig> for Network {
|
||||
fn from(value: NetworkConfig) -> Self {
|
||||
let default_refresh_interval = 10;
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
@@ -65,17 +71,25 @@ impl From<NetworkConfig> for Network {
|
||||
show_total_activity: value.show_total_activity,
|
||||
show_activity: value.show_activity,
|
||||
show_default_interface: value.show_default_interface.unwrap_or(true),
|
||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
||||
default_interface: String::new(),
|
||||
networks_network_activity: Arc::new(Mutex::new(Networks::new_with_refreshed_list())),
|
||||
default_interface: Arc::new(Mutex::new(String::new())),
|
||||
interface_generation: Arc::new(AtomicU64::new(0)),
|
||||
default_refresh_interval,
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
auto_select: value.auto_select,
|
||||
activity_left_padding: value.activity_left_padding.unwrap_or_default(),
|
||||
last_state_total_activity: vec![],
|
||||
last_state_activity: vec![],
|
||||
last_updated_network_activity: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
last_update_request_default_interface: Instant::now()
|
||||
.checked_sub(Duration::from_secs(default_refresh_interval))
|
||||
.unwrap(),
|
||||
last_state_total_activity: Arc::new(Mutex::new(vec![])),
|
||||
last_state_activity: Arc::new(Mutex::new(vec![])),
|
||||
last_update_request_network_activity: Arc::new(Mutex::new(
|
||||
Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
)),
|
||||
activity_generation: Arc::new(AtomicU64::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,84 +99,139 @@ pub struct Network {
|
||||
pub show_total_activity: bool,
|
||||
pub show_activity: bool,
|
||||
pub show_default_interface: bool,
|
||||
networks_network_activity: Networks,
|
||||
networks_network_activity: Arc<Mutex<Networks>>,
|
||||
default_refresh_interval: u64,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select: Option<NetworkSelectConfig>,
|
||||
default_interface: String,
|
||||
last_state_total_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
last_updated_network_activity: Instant,
|
||||
default_interface: Arc<Mutex<String>>,
|
||||
interface_generation: Arc<AtomicU64>,
|
||||
last_update_request_default_interface: Instant,
|
||||
activity_generation: Arc<AtomicU64>,
|
||||
last_state_activity: Arc<Mutex<Vec<NetworkReading>>>,
|
||||
last_state_total_activity: Arc<Mutex<Vec<NetworkReading>>>,
|
||||
last_update_request_network_activity: Arc<Mutex<Instant>>,
|
||||
activity_left_padding: usize,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
fn default_interface(&mut self) {
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
fn update_default_interface_async(&mut self) {
|
||||
let gen_ = self.interface_generation.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
let gen_arc = Arc::clone(&self.interface_generation);
|
||||
let iface_arc = Arc::clone(&self.default_interface);
|
||||
|
||||
thread::spawn(move || {
|
||||
if let Ok(interface) = netdev::get_default_interface()
|
||||
&& let Some(friendly_name) = &interface.friendly_name
|
||||
{
|
||||
// Only update if this is the latest request
|
||||
if gen_ == gen_arc.load(Ordering::SeqCst)
|
||||
&& let Ok(mut iface) = iface_arc.lock()
|
||||
{
|
||||
*iface = friendly_name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let mut activity = self.last_state_activity.clone();
|
||||
let mut total_activity = self.last_state_total_activity.clone();
|
||||
fn default_interface(&mut self) -> String {
|
||||
let current = self.default_interface.lock().unwrap().clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
if now.duration_since(self.last_update_request_default_interface)
|
||||
> Duration::from_secs(self.default_refresh_interval)
|
||||
{
|
||||
activity.clear();
|
||||
total_activity.clear();
|
||||
self.last_update_request_default_interface = now;
|
||||
self.update_default_interface_async();
|
||||
}
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
current
|
||||
}
|
||||
|
||||
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 {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if self.show_activity {
|
||||
let received = Self::to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval,
|
||||
);
|
||||
let transmitted = Self::to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval,
|
||||
);
|
||||
thread::spawn(move || {
|
||||
let mut activity = Vec::new();
|
||||
let mut total_activity = Vec::new();
|
||||
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
ReadingValue::from(received),
|
||||
ReadingValue::from(transmitted),
|
||||
));
|
||||
}
|
||||
if let Ok(interface) = netdev::get_default_interface()
|
||||
&& let Some(friendly_name) = &interface.friendly_name
|
||||
&& let Ok(mut networks) = networks_network_activity_arc.lock()
|
||||
{
|
||||
networks.refresh(true);
|
||||
|
||||
if self.show_total_activity {
|
||||
let total_received =
|
||||
Self::to_pretty_bytes(data.total_received(), 1);
|
||||
let total_transmitted =
|
||||
Self::to_pretty_bytes(data.total_transmitted(), 1);
|
||||
for (interface_name, data) in &*networks {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if show_activity {
|
||||
let received =
|
||||
Network::to_pretty_bytes(data.received(), data_refresh_interval);
|
||||
let transmitted =
|
||||
Network::to_pretty_bytes(data.transmitted(), data_refresh_interval);
|
||||
|
||||
total_activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Total,
|
||||
ReadingValue::from(total_received),
|
||||
ReadingValue::from(total_transmitted),
|
||||
))
|
||||
}
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
ReadingValue::from(received),
|
||||
ReadingValue::from(transmitted),
|
||||
));
|
||||
}
|
||||
|
||||
if show_total_activity {
|
||||
let total_received = Network::to_pretty_bytes(data.total_received(), 1);
|
||||
let total_transmitted =
|
||||
Network::to_pretty_bytes(data.total_transmitted(), 1);
|
||||
|
||||
total_activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Total,
|
||||
ReadingValue::from(total_received),
|
||||
ReadingValue::from(total_transmitted),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_activity.clone_from(&activity);
|
||||
self.last_state_total_activity.clone_from(&total_activity);
|
||||
self.last_updated_network_activity = now;
|
||||
// Only update if this is the latest request
|
||||
if gen_ == gen_arc.load(Ordering::SeqCst) {
|
||||
if let Ok(mut act) = activity_arc.lock() {
|
||||
*act = activity;
|
||||
}
|
||||
if let Ok(mut tot) = total_activity_arc.lock() {
|
||||
*tot = total_activity;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let now = Instant::now();
|
||||
let should_update = {
|
||||
let last_update_request = self.last_update_request_network_activity.lock().unwrap();
|
||||
now.duration_since(*last_update_request)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
};
|
||||
|
||||
if should_update {
|
||||
{
|
||||
let mut last_updated = self.last_update_request_network_activity.lock().unwrap();
|
||||
*last_updated = now;
|
||||
}
|
||||
self.update_network_activity_async();
|
||||
}
|
||||
|
||||
self.get_network_activity()
|
||||
}
|
||||
|
||||
fn get_network_activity(&self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let activity = self.last_state_activity.lock().unwrap().clone();
|
||||
let total_activity = self.last_state_total_activity.lock().unwrap().clone();
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
@@ -312,10 +381,9 @@ impl Network {
|
||||
if SelectableFrame::new_auto(selected, auto_focus_fill)
|
||||
.show(ui, add_contents)
|
||||
.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 +502,9 @@ impl BarWidget for Network {
|
||||
}
|
||||
|
||||
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(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
@@ -450,11 +518,11 @@ impl BarWidget for Network {
|
||||
);
|
||||
|
||||
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(
|
||||
&self.default_interface,
|
||||
&self_default_interface,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
@@ -535,6 +603,6 @@ enum DataUnit {
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -25,6 +25,10 @@ pub struct StorageConfig {
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Show disks that are read only. (default: false)
|
||||
pub show_read_only_disks: Option<bool>,
|
||||
/// Show removable disks. (default: true)
|
||||
pub show_removable_disks: Option<bool>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
/// Hide when the current percentage is under this value [[1-100]]
|
||||
@@ -38,6 +42,8 @@ impl From<StorageConfig> for Storage {
|
||||
disks: Disks::new_with_refreshed_list(),
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
|
||||
show_removable_disks: value.show_removable_disks.unwrap_or(true),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now(),
|
||||
@@ -55,6 +61,8 @@ pub struct Storage {
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
show_read_only_disks: bool,
|
||||
show_removable_disks: bool,
|
||||
auto_select_over: Option<u8>,
|
||||
auto_hide_under: Option<u8>,
|
||||
last_updated: Instant,
|
||||
@@ -71,6 +79,12 @@ impl Storage {
|
||||
let mut disks = vec![];
|
||||
|
||||
for disk in &self.disks {
|
||||
if disk.is_read_only() && !self.show_read_only_disks {
|
||||
continue;
|
||||
}
|
||||
if disk.is_removable() && !self.show_removable_disks {
|
||||
continue;
|
||||
}
|
||||
let mount = disk.mount_point();
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
@@ -87,7 +101,7 @@ impl Storage {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), percentage)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
|
||||
},
|
||||
selected,
|
||||
})
|
||||
@@ -142,17 +156,15 @@ impl BarWidget for Storage {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
&& let Err(error) = Command::new("cmd.exe")
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.label.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
{
|
||||
eprintln!("{error}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::widgets::widget::BarWidget;
|
||||
use chrono::Local;
|
||||
use chrono::NaiveTime;
|
||||
use chrono_tz::Tz;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
@@ -16,6 +15,7 @@ use eframe::egui::Stroke;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::epaint::StrokeKind;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
@@ -209,7 +209,7 @@ impl Time {
|
||||
Some(dt.time()),
|
||||
)
|
||||
}
|
||||
Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
|
||||
Err(_) => (format!("Invalid timezone: {timezone:?}"), None),
|
||||
},
|
||||
None => {
|
||||
let dt = Local::now();
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -140,16 +140,14 @@ impl BarWidget for Update {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("explorer.exe")
|
||||
&& let Err(error) = Command::new("explorer.exe")
|
||||
.args([format!(
|
||||
"https://github.com/LGUG2Z/komorebi/releases/v{}",
|
||||
self.latest_version
|
||||
)])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
{
|
||||
eprintln!("{error}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widgets::applications::Applications;
|
||||
use crate::widgets::applications::ApplicationsConfig;
|
||||
use crate::widgets::battery::Battery;
|
||||
use crate::widgets::battery::BatteryConfig;
|
||||
use crate::widgets::cpu::Cpu;
|
||||
@@ -33,6 +35,7 @@ pub trait BarWidget {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum WidgetConfig {
|
||||
Applications(ApplicationsConfig),
|
||||
Battery(BatteryConfig),
|
||||
Cpu(CpuConfig),
|
||||
Date(DateConfig),
|
||||
@@ -49,6 +52,7 @@ pub enum WidgetConfig {
|
||||
impl WidgetConfig {
|
||||
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
|
||||
match self {
|
||||
WidgetConfig::Applications(config) => Box::new(Applications::from(config)),
|
||||
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
|
||||
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
|
||||
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
|
||||
@@ -65,6 +69,7 @@ impl WidgetConfig {
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
match self {
|
||||
WidgetConfig::Applications(config) => config.enable,
|
||||
WidgetConfig::Battery(config) => config.enable,
|
||||
WidgetConfig::Cpu(config) => config.enable,
|
||||
WidgetConfig::Date(config) => config.enable,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi = { path = "../komorebi" }
|
||||
komorebi = { path = "../komorebi", default-features = false }
|
||||
|
||||
uds_windows = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["komorebi/schemars"]
|
||||
schemars = ["komorebi/default"]
|
||||
|
||||
@@ -1,8 +1,36 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::animation::prefix::AnimationPrefix;
|
||||
pub use komorebi::AnimationsConfig;
|
||||
pub use komorebi::AppSpecificConfigurationPath;
|
||||
pub use komorebi::AspectRatio;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::Colour;
|
||||
pub use komorebi::CrossBoundaryBehaviour;
|
||||
pub use komorebi::GridLayoutOptions;
|
||||
pub use komorebi::KomorebiTheme;
|
||||
pub use komorebi::LayoutOptions;
|
||||
pub use komorebi::MonitorConfig;
|
||||
pub use komorebi::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::Placement;
|
||||
pub use komorebi::PredefinedAspectRatio;
|
||||
pub use komorebi::Rgb;
|
||||
pub use komorebi::RuleDebug;
|
||||
pub use komorebi::ScrollingLayoutOptions;
|
||||
pub use komorebi::StackbarConfig;
|
||||
pub use komorebi::StaticConfig;
|
||||
pub use komorebi::SubscribeOptions;
|
||||
pub use komorebi::TabsConfig;
|
||||
pub use komorebi::ThemeOptions;
|
||||
pub use komorebi::VirtualDesktopNotification;
|
||||
pub use komorebi::Wallpaper;
|
||||
pub use komorebi::WindowContainerBehaviour;
|
||||
pub use komorebi::WindowHandlingBehaviour;
|
||||
pub use komorebi::WindowsApi;
|
||||
pub use komorebi::WorkspaceConfig;
|
||||
pub use komorebi::animation::PerAnimationPrefixConfig;
|
||||
pub use komorebi::animation::prefix::AnimationPrefix;
|
||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||
pub use komorebi::border_manager::BorderInfo;
|
||||
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::MatchingStrategy;
|
||||
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::ApplicationIdentifier;
|
||||
pub use komorebi::core::Arrangement;
|
||||
@@ -42,40 +68,24 @@ pub use komorebi::core::StackbarLabel;
|
||||
pub use komorebi::core::StackbarMode;
|
||||
pub use komorebi::core::StateQuery;
|
||||
pub use komorebi::core::WindowKind;
|
||||
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
||||
pub use komorebi::core::replace_env_in_path;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
pub use komorebi::monitor_reconciliator::MonitorNotification;
|
||||
pub use komorebi::ring::Ring;
|
||||
pub use komorebi::splash;
|
||||
pub use komorebi::state::GlobalState;
|
||||
pub use komorebi::state::State;
|
||||
pub use komorebi::win32_display_data;
|
||||
pub use komorebi::window::Window;
|
||||
pub use komorebi::window_manager_event::WindowManagerEvent;
|
||||
pub use komorebi::workspace::Workspace;
|
||||
pub use komorebi::workspace::WorkspaceGlobals;
|
||||
pub use komorebi::workspace::WorkspaceLayer;
|
||||
pub use komorebi::AnimationsConfig;
|
||||
pub use komorebi::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::WindowContainerBehaviour;
|
||||
pub use komorebi::WindowsApi;
|
||||
pub use komorebi::WorkspaceConfig;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
@@ -93,12 +103,15 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
|
||||
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()>
|
||||
where
|
||||
Q: Borrow<SocketMessage>,
|
||||
{
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
|
||||
if let Ok(m_str) = serde_json::to_string(&m) {
|
||||
if let Ok(m_str) = serde_json::to_string(m.borrow()) {
|
||||
s.push_str(&m_str);
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
use eframe::egui;
|
||||
use eframe::egui::color_picker::Alpha;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use eframe::egui::color_picker::Alpha;
|
||||
use komorebi_client::BorderStyle;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::DefaultLayout;
|
||||
@@ -78,8 +78,8 @@ impl From<&komorebi_client::Monitor> for MonitorConfig {
|
||||
}
|
||||
|
||||
Self {
|
||||
size: *value.size(),
|
||||
work_area_offset: value.work_area_offset().unwrap_or_default(),
|
||||
size: value.size,
|
||||
work_area_offset: value.work_area_offset.unwrap_or_default(),
|
||||
workspaces,
|
||||
}
|
||||
}
|
||||
@@ -95,22 +95,22 @@ struct WorkspaceConfig {
|
||||
|
||||
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
|
||||
fn from(value: &komorebi_client::Workspace) -> Self {
|
||||
let layout = match value.layout() {
|
||||
Layout::Default(layout) => *layout,
|
||||
let layout = match value.layout {
|
||||
Layout::Default(layout) => layout,
|
||||
Layout::Custom(_) => DefaultLayout::BSP,
|
||||
};
|
||||
|
||||
let name = value
|
||||
.name()
|
||||
.name
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
|
||||
|
||||
Self {
|
||||
layout,
|
||||
name,
|
||||
tile: *value.tile(),
|
||||
workspace_padding: value.workspace_padding().unwrap_or(20),
|
||||
container_padding: value.container_padding().unwrap_or(20),
|
||||
tile: value.tile,
|
||||
workspace_padding: value.workspace_padding.unwrap_or(20),
|
||||
container_padding: value.container_padding.unwrap_or(20),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,7 +247,7 @@ impl eframe::App for KomorebiGui {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ctx.set_pixels_per_point(2.0);
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.set_width(ctx.screen_rect().width());
|
||||
ui.set_width(ctx.content_rect().width());
|
||||
ui.collapsing("Debugging", |ui| {
|
||||
ui.collapsing("Window Rules", |ui| {
|
||||
let window = Window::from(self.debug_hwnd);
|
||||
@@ -437,7 +437,7 @@ impl eframe::App for KomorebiGui {
|
||||
BorderStyle::Square,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
.add(egui::Button::selectable(
|
||||
self.border_config.border_style == option,
|
||||
option.to_string(),
|
||||
))
|
||||
@@ -494,7 +494,7 @@ impl eframe::App for KomorebiGui {
|
||||
StackbarMode::Always,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
.add(egui::Button::selectable(
|
||||
self.stackbar_config.mode == option,
|
||||
option.to_string(),
|
||||
))
|
||||
@@ -513,7 +513,7 @@ impl eframe::App for KomorebiGui {
|
||||
ui.collapsing("Label", |ui| {
|
||||
for option in [StackbarLabel::Process, StackbarLabel::Title] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
.add(egui::Button::selectable(
|
||||
self.stackbar_config.label == option,
|
||||
option.to_string(),
|
||||
))
|
||||
@@ -772,7 +772,7 @@ impl eframe::App for KomorebiGui {
|
||||
DefaultLayout::Grid,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
.add(egui::Button::selectable(
|
||||
workspace.layout == option,
|
||||
option.to_string(),
|
||||
))
|
||||
|
||||
11
komorebi-shortcuts/Cargo.toml
Normal file
11
komorebi-shortcuts/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "komorebi-shortcuts"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.10" }
|
||||
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.10" }
|
||||
|
||||
eframe = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
98
komorebi-shortcuts/src/main.rs
Normal file
98
komorebi-shortcuts/src/main.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use std::path::PathBuf;
|
||||
use whkd_core::Whkdrc;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Quicklook {
|
||||
whkdrc: Option<Whkdrc>,
|
||||
filter: String,
|
||||
}
|
||||
|
||||
impl Quicklook {
|
||||
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
|
||||
// Restore app state using cc.storage (requires the "persistence" feature).
|
||||
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
|
||||
// for e.g. egui::PaintCallback.
|
||||
let mut home = std::env::var("WHKD_CONFIG_HOME").map_or_else(
|
||||
|_| {
|
||||
dirs::home_dir()
|
||||
.expect("no home directory found")
|
||||
.join(".config")
|
||||
},
|
||||
|home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!(
|
||||
"$Env:WHKD_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
home.push("whkdrc");
|
||||
|
||||
Self {
|
||||
whkdrc: whkd_parser::load(&home).ok(),
|
||||
filter: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Quicklook {
|
||||
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
|
||||
eframe::egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.set_max_width(ui.available_width());
|
||||
ui.set_max_height(ui.available_height());
|
||||
eframe::egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
eframe::egui::Grid::new("grid")
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.spacing([40.0, 4.0])
|
||||
.min_col_width(ui.available_width() / 2.0 - 20.0)
|
||||
.show(ui, |ui| {
|
||||
if let Some(whkdrc) = &self.whkdrc {
|
||||
ui.label("Filter");
|
||||
ui.add(
|
||||
eframe::egui::text_edit::TextEdit::singleline(&mut self.filter)
|
||||
.hint_text("Filter by command...")
|
||||
.background_color(ctx.style().visuals.faint_bg_color),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
for binding in &whkdrc.bindings {
|
||||
let keys = binding.keys.join(" + ");
|
||||
if self.filter.is_empty() || binding.command.contains(&self.filter)
|
||||
{
|
||||
ui.label(keys);
|
||||
ui.label(&binding.command);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let viewport_builder = ViewportBuilder::default()
|
||||
.with_resizable(true)
|
||||
.with_decorations(false);
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: viewport_builder,
|
||||
centered: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"komorebi-shortcuts",
|
||||
native_options,
|
||||
Box::new(|cc| Ok(Box::new(Quicklook::new(cc)))),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "96f26c88d83781f234d42222293ec73d23a39ad8" }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "bdaff30959512c4f7ee7304117076a48633d777f", default-features = false, features = ["egui31"] }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "5472b1ab825c48af1a1726e324cfa13b7c385135" }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a", default-features = false, features = ["egui33"] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use hex_color::HexColor;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::SchemaGenerator;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::InstanceType;
|
||||
#[cfg(feature = "schemars")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::Base16ColourPalette;
|
||||
use crate::colour::Colour;
|
||||
use crate::colour::Hex;
|
||||
use crate::Base16ColourPalette;
|
||||
use hex_color::HexColor;
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Display;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
pub mod colour;
|
||||
mod generator;
|
||||
|
||||
pub use generator::generate_base16_palette;
|
||||
pub use generator::ThemeVariant;
|
||||
pub use generator::generate_base16_palette;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -16,14 +16,14 @@ use strum::IntoEnumIterator;
|
||||
use crate::colour::Colour;
|
||||
pub use base16_egui_themes::Base16;
|
||||
pub use catppuccin_egui;
|
||||
use eframe::egui::style::Selection;
|
||||
use eframe::egui::style::WidgetVisuals;
|
||||
use eframe::egui::style::Widgets;
|
||||
pub use eframe::egui::Color32;
|
||||
use eframe::egui::Shadow;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::Visuals;
|
||||
use eframe::egui::style::Selection;
|
||||
use eframe::egui::style::WidgetVisuals;
|
||||
use eframe::egui::style::Widgets;
|
||||
use serde_variant::to_variant_name;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.36"
|
||||
version = "0.1.40"
|
||||
description = "A tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
|
||||
base64 = "0.22"
|
||||
bitflags = { version = "2", features = ["serde"] }
|
||||
clap = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
crossbeam-utils = { workspace = true }
|
||||
ctrlc = { version = "3", features = ["termination"] }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
getset = "0.1"
|
||||
ed25519-dalek = "2"
|
||||
hotwatch = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
miow = "0.6"
|
||||
nanoid = "0.4"
|
||||
net2 = "0.2"
|
||||
os_info = "3.10"
|
||||
parking_lot = "0.12"
|
||||
parking_lot = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
powershell_script = "1.0"
|
||||
regex = "1"
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
serde_yaml = { workspace = true }
|
||||
shadow-rs = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use super::prefix::AnimationPrefix;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -6,10 +6,10 @@ use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::RenderDispatcher;
|
||||
use super::ANIMATION_DURATION_GLOBAL;
|
||||
use super::ANIMATION_FPS;
|
||||
use super::ANIMATION_MANAGER;
|
||||
use super::RenderDispatcher;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
@@ -55,9 +55,9 @@ impl AnimationEngine {
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
render_dispatcher: (impl RenderDispatcher + Send + 'static),
|
||||
render_dispatcher: impl RenderDispatcher + Send + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<()> {
|
||||
) -> eyre::Result<()> {
|
||||
std::thread::spawn(move || {
|
||||
let animation_key = render_dispatcher.get_animation_key();
|
||||
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::core::Rect;
|
||||
use crate::AnimationStyle;
|
||||
use crate::core::Rect;
|
||||
|
||||
use super::style::apply_ease_func;
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ use crate::core::animation::AnimationStyle;
|
||||
use lazy_static::lazy_static;
|
||||
use prefix::AnimationPrefix;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
|
||||
@@ -17,5 +17,5 @@ pub enum AnimationPrefix {
|
||||
}
|
||||
|
||||
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
|
||||
format!("{}:{}", prefix, key)
|
||||
format!("{prefix}:{key}")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
|
||||
pub trait RenderDispatcher {
|
||||
fn get_animation_key(&self) -> String;
|
||||
fn pre_render(&self) -> Result<()>;
|
||||
fn render(&self, delta: f64) -> Result<()>;
|
||||
fn post_render(&self) -> Result<()>;
|
||||
fn pre_render(&self) -> eyre::Result<()>;
|
||||
fn render(&self, delta: f64) -> eyre::Result<()>;
|
||||
fn post_render(&self) -> eyre::Result<()>;
|
||||
}
|
||||
|
||||
@@ -355,6 +355,61 @@ impl Ease for EaseInOutBounce {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CubicBezier {
|
||||
pub x1: f64,
|
||||
pub y1: f64,
|
||||
pub x2: f64,
|
||||
pub y2: f64,
|
||||
}
|
||||
|
||||
impl CubicBezier {
|
||||
fn x(&self, s: f64) -> f64 {
|
||||
3.0 * self.x1 * s * (1.0 - s).powi(2) + 3.0 * self.x2 * s.powi(2) * (1.0 - s) + s.powi(3)
|
||||
}
|
||||
|
||||
fn y(&self, s: f64) -> f64 {
|
||||
3.0 * self.y1 * s * (1.0 - s).powi(2) + 3.0 * self.y2 * s.powi(2) * (1.0 - s) + s.powi(3)
|
||||
}
|
||||
|
||||
fn dx_ds(&self, s: f64) -> f64 {
|
||||
3.0 * self.x1 * (1.0 - s) * (1.0 - 3.0 * s)
|
||||
+ 3.0 * self.x2 * (2.0 * s - 3.0 * s.powi(2))
|
||||
+ 3.0 * s.powi(2)
|
||||
}
|
||||
|
||||
fn find_s(&self, t: f64) -> f64 {
|
||||
if t <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if t >= 1.0 {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
let mut s = t;
|
||||
|
||||
for _ in 0..8 {
|
||||
let x_val = self.x(s);
|
||||
let dx_val = self.dx_ds(s);
|
||||
if dx_val.abs() < 1e-6 {
|
||||
break;
|
||||
}
|
||||
let delta = (x_val - t) / dx_val;
|
||||
s = (s - delta).clamp(0.0, 1.0);
|
||||
if delta.abs() < 1e-6 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
fn evaluate(&self, t: f64) -> f64 {
|
||||
let s = self.find_s(t.clamp(0.0, 1.0));
|
||||
self.y(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
match style {
|
||||
AnimationStyle::Linear => Linear::evaluate(t),
|
||||
@@ -387,5 +442,6 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t),
|
||||
AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
|
||||
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
|
||||
AnimationStyle::CubicBezier(x1, y1, x2, y2) => CubicBezier { x1, y1, x2, y2 }.evaluate(t),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::border_manager::RenderTarget;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::WINDOWS_11;
|
||||
use crate::WindowsApi;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::RenderTarget;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::LazyLock;
|
||||
use windows::Win32::Foundation::FALSE;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::TRUE;
|
||||
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_COLOR_F;
|
||||
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_BRUSH_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
|
||||
@@ -36,31 +33,34 @@ 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_TYPE_DEFAULT;
|
||||
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_ENABLE;
|
||||
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::Gdi::CreateRectRgn;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::ValidateRect;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
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_LOCATIONCHANGE;
|
||||
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::LoadCursorW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
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_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
@@ -102,10 +102,10 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||
let hwnd = hwnd.0 as isize;
|
||||
|
||||
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
|
||||
if class.starts_with("komoborder") {
|
||||
hwnds.push(hwnd);
|
||||
}
|
||||
if let Ok(class) = WindowsApi::real_window_class_w(hwnd)
|
||||
&& class.starts_with("komoborder")
|
||||
{
|
||||
hwnds.push(hwnd);
|
||||
}
|
||||
|
||||
true.into()
|
||||
@@ -392,63 +392,63 @@ impl Border {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if !rect.is_same_size_as(&old_rect) {
|
||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
if (!rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect))
|
||||
&& let Some(render_target) = (*border_pointer).render_target.as_ref()
|
||||
{
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod border;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::WindowKind;
|
||||
use crate::ring::Ring;
|
||||
use crate::windows_api;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
pub use border::Border;
|
||||
use border::border_hwnds;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
@@ -21,15 +22,15 @@ use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use strum::Display;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
@@ -105,11 +106,10 @@ fn event_rx() -> Receiver<Notification> {
|
||||
}
|
||||
|
||||
pub fn window_border(hwnd: isize) -> Option<BorderInfo> {
|
||||
WINDOWS_BORDERS.lock().get(&hwnd).and_then(|id| {
|
||||
BORDER_STATE.lock().get(id).map(|b| BorderInfo {
|
||||
border_hwnd: b.hwnd,
|
||||
window_kind: b.window_kind,
|
||||
})
|
||||
let id = WINDOWS_BORDERS.lock().get(&hwnd)?.clone();
|
||||
BORDER_STATE.lock().get(&id).map(|b| BorderInfo {
|
||||
border_hwnd: b.hwnd,
|
||||
window_kind: b.window_kind,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -136,6 +136,8 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
let _ = destroy_border(border);
|
||||
}
|
||||
|
||||
drop(borders);
|
||||
|
||||
WINDOWS_BORDERS.lock().clear();
|
||||
|
||||
let mut remaining_hwnds = vec![];
|
||||
@@ -168,13 +170,15 @@ fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -207,10 +211,12 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.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]
|
||||
.layer();
|
||||
.layer;
|
||||
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
let forced_update = matches!(notification, Notification::ForceUpdate);
|
||||
|
||||
drop(state);
|
||||
|
||||
@@ -220,7 +226,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// 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 {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
@@ -233,6 +239,17 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
|
||||
if ws.layer == WorkspaceLayer::Floating {
|
||||
for window in ws.floating_windows() {
|
||||
let mut window_kind = WindowKind::Unfocused;
|
||||
|
||||
if foreground_window == window.hwnd {
|
||||
window_kind = WindowKind::Floating;
|
||||
}
|
||||
|
||||
window.set_accent(window_kind_colour(window_kind))?;
|
||||
}
|
||||
}
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
@@ -240,7 +257,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let window_kind = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
if c.locked {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
@@ -324,15 +341,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
if let Some(Notification::Update(ref previous)) = previous_notification
|
||||
{
|
||||
if previous.unwrap_or_default()
|
||||
!= notification_hwnd.unwrap_or_default()
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
}
|
||||
if !should_process_notification
|
||||
&& let Some(Notification::Update(ref previous)) = previous_notification
|
||||
&& previous.unwrap_or_default() != notification_hwnd.unwrap_or_default()
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
should_process_notification
|
||||
@@ -341,7 +354,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
};
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
tracing::debug!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
@@ -368,7 +381,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Workspaces with tiling disabled don't have borders
|
||||
if !ws.tile() {
|
||||
if !ws.tile {
|
||||
// Remove all borders on this monitor
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
@@ -381,16 +394,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
// 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 focused_window_hwnd =
|
||||
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()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(
|
||||
monocle.id(),
|
||||
&monocle.id,
|
||||
focused_window_hwnd,
|
||||
monitor_idx,
|
||||
) {
|
||||
@@ -447,22 +460,48 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
windows_borders.insert(focused_window_hwnd, id);
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
// Remove all borders on this monitor except monocle
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| border_hwnd != b.hwnd,
|
||||
)?;
|
||||
|
||||
if ws.layer == WorkspaceLayer::Floating {
|
||||
handle_floating_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
ws,
|
||||
monitor_idx,
|
||||
foreground_window,
|
||||
layer_changed,
|
||||
forced_update,
|
||||
)?;
|
||||
|
||||
// Remove all borders on this monitor except monocle and floating borders
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| {
|
||||
border_hwnd != b.hwnd
|
||||
&& !ws
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.any(|w| w.hwnd == b.tracking_hwnd)
|
||||
},
|
||||
)?;
|
||||
} else {
|
||||
// Remove all borders on this monitor except monocle
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| border_hwnd != b.hwnd,
|
||||
)?;
|
||||
}
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let foreground_monitor_id =
|
||||
WindowsApi::monitor_from_window(foreground_hwnd);
|
||||
let is_maximized = foreground_monitor_id == m.id()
|
||||
&& WindowsApi::is_zoomed(foreground_hwnd);
|
||||
let is_maximized =
|
||||
foreground_monitor_id == m.id && WindowsApi::is_zoomed(foreground_hwnd);
|
||||
|
||||
if is_maximized {
|
||||
// Remove all borders on this monitor
|
||||
@@ -480,7 +519,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let mut container_and_floating_window_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
.map(|c| c.id().clone())
|
||||
.map(|c| c.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for w in ws.floating_windows() {
|
||||
@@ -498,7 +537,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
'containers: for (idx, c) in ws.containers().iter().enumerate() {
|
||||
let focused_window_hwnd =
|
||||
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
|
||||
let mut new_border = false;
|
||||
@@ -506,7 +545,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
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;
|
||||
entry.insert(border)
|
||||
@@ -522,7 +561,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
|| focused_window_hwnd != foreground_window
|
||||
{
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
if c.locked {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
@@ -562,15 +601,12 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let rect = match WindowsApi::window_rect(focused_window_hwnd) {
|
||||
Ok(rect) => rect,
|
||||
Err(_) => {
|
||||
remove_border(c.id(), &mut borders, &mut windows_borders)?;
|
||||
remove_border(&c.id, &mut borders, &mut windows_borders)?;
|
||||
continue 'containers;
|
||||
}
|
||||
};
|
||||
border.window_rect = rect;
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
let forced_update = matches!(notification, Notification::ForceUpdate);
|
||||
|
||||
let should_invalidate = new_border
|
||||
|| (last_focus_state != new_focus_state)
|
||||
|| layer_changed
|
||||
@@ -590,65 +626,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
windows_borders.insert(focused_window_hwnd, id);
|
||||
}
|
||||
|
||||
{
|
||||
for window in ws.floating_windows() {
|
||||
let mut new_border = false;
|
||||
let id = window.hwnd.to_string();
|
||||
let border = match borders.entry(id.clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(
|
||||
&window.hwnd.to_string(),
|
||||
window.hwnd,
|
||||
monitor_idx,
|
||||
) {
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let last_focus_state = border.window_kind;
|
||||
|
||||
let new_focus_state = if foreground_window == window.hwnd {
|
||||
WindowKind::Floating
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
};
|
||||
|
||||
border.window_kind = new_focus_state;
|
||||
|
||||
// Update the border's monitor idx in case it changed
|
||||
border.monitor_idx = Some(monitor_idx);
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
border.window_rect = rect;
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
let forced_update =
|
||||
matches!(notification, Notification::ForceUpdate);
|
||||
|
||||
let should_invalidate = new_border
|
||||
|| (last_focus_state != new_focus_state)
|
||||
|| layer_changed
|
||||
|| forced_update;
|
||||
|
||||
if should_invalidate {
|
||||
if forced_update && !new_border {
|
||||
// Update the border brushes if there was a forced update
|
||||
// notification and this is not a new border (new border's
|
||||
// already have their brushes updated on creation)
|
||||
border.update_brushes()?;
|
||||
}
|
||||
border.set_position(&rect, window.hwnd)?;
|
||||
border.invalidate();
|
||||
}
|
||||
|
||||
windows_borders.insert(window.hwnd, id);
|
||||
}
|
||||
}
|
||||
handle_floating_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
ws,
|
||||
monitor_idx,
|
||||
foreground_window,
|
||||
layer_changed,
|
||||
forced_update,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -664,6 +650,68 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_floating_borders(
|
||||
borders: &mut HashMap<String, Box<Border>>,
|
||||
windows_borders: &mut HashMap<isize, String>,
|
||||
ws: &Workspace,
|
||||
monitor_idx: usize,
|
||||
foreground_window: isize,
|
||||
layer_changed: bool,
|
||||
forced_update: bool,
|
||||
) -> color_eyre::Result<()> {
|
||||
for window in ws.floating_windows() {
|
||||
let mut new_border = false;
|
||||
let id = window.hwnd.to_string();
|
||||
let border = match borders.entry(id.clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) =
|
||||
Border::create(&window.hwnd.to_string(), window.hwnd, monitor_idx)
|
||||
{
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let last_focus_state = border.window_kind;
|
||||
|
||||
let new_focus_state = if foreground_window == window.hwnd {
|
||||
WindowKind::Floating
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
};
|
||||
|
||||
border.window_kind = new_focus_state;
|
||||
|
||||
// Update the border's monitor idx in case it changed
|
||||
border.monitor_idx = Some(monitor_idx);
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
border.window_rect = rect;
|
||||
|
||||
let should_invalidate =
|
||||
new_border || (last_focus_state != new_focus_state) || layer_changed || forced_update;
|
||||
|
||||
if should_invalidate {
|
||||
if forced_update && !new_border {
|
||||
// Update the border brushes if there was a forced update
|
||||
// notification and this is not a new border (new border's
|
||||
// already have their brushes updated on creation)
|
||||
border.update_brushes()?;
|
||||
}
|
||||
border.set_position(&rect, window.hwnd)?;
|
||||
border.invalidate();
|
||||
}
|
||||
|
||||
windows_borders.insert(window.hwnd, id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes all borders from monitor with index `monitor_idx` filtered by
|
||||
/// `condition`. This condition is a function that will take a reference to
|
||||
/// the container id and the border and returns a bool, if true that border
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
|
||||
use std::ffi::c_void;
|
||||
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::RECT;
|
||||
use windows::Win32::Foundation::SIZE;
|
||||
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;
|
||||
|
||||
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 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)
|
||||
-> HRESULT;
|
||||
-> HRESULT;
|
||||
|
||||
/*** IApplicationView methods ***/
|
||||
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 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::CoInitializeEx;
|
||||
use windows::Win32::System::Com::CoUninitialize;
|
||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||
use windows::Win32::System::Com::COINIT_MULTITHREADED;
|
||||
use windows_core::Interface;
|
||||
|
||||
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) {
|
||||
COM_INIT.with(|_| {
|
||||
let provider = get_iservice_provider();
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use getset::Getters;
|
||||
use nanoid::nanoid;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Lockable;
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Container {
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub locked: bool,
|
||||
windows: Ring<Window>,
|
||||
}
|
||||
|
||||
@@ -22,22 +23,45 @@ impl Default for Container {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: nanoid!(),
|
||||
locked: false,
|
||||
windows: Ring::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Lockable for Container {
|
||||
fn locked(&self) -> bool {
|
||||
self.locked
|
||||
}
|
||||
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self {
|
||||
self.locked = locked;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
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>) {
|
||||
for window in self.windows().iter().rev() {
|
||||
let mut should_hide = omit.is_none();
|
||||
|
||||
if !should_hide {
|
||||
if let Some(omit) = omit {
|
||||
if omit != window.hwnd {
|
||||
should_hide = true
|
||||
}
|
||||
}
|
||||
if !should_hide
|
||||
&& let Some(omit) = omit
|
||||
&& omit != window.hwnd
|
||||
{
|
||||
should_hide = true
|
||||
}
|
||||
|
||||
if should_hide {
|
||||
@@ -69,10 +93,10 @@ impl Container {
|
||||
|
||||
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
|
||||
for window in self.windows() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(window.hwnd);
|
||||
}
|
||||
if let Ok(window_exe) = window.exe()
|
||||
&& exe == window_exe
|
||||
{
|
||||
return Option::from(window.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +105,10 @@ impl Container {
|
||||
|
||||
pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
|
||||
for (idx, window) in self.windows().iter().enumerate() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(idx);
|
||||
}
|
||||
if let Ok(window_exe) = window.exe()
|
||||
&& exe == window_exe
|
||||
{
|
||||
return Option::from(idx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +168,7 @@ impl Container {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json;
|
||||
|
||||
#[test]
|
||||
fn test_contains_window() {
|
||||
@@ -250,4 +275,40 @@ mod tests {
|
||||
// Should return None since window 4 doesn't exist
|
||||
assert_eq!(container.idx_for_window(4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_with_missing_locked_field_defaults_to_false() {
|
||||
let json = r#"{
|
||||
"id": "test-1",
|
||||
"windows": { "elements": [], "focused": 0 }
|
||||
}"#;
|
||||
let container: Container = serde_json::from_str(json).expect("Should deserialize");
|
||||
|
||||
assert!(!container.locked);
|
||||
assert_eq!(container.id, "test-1");
|
||||
assert!(container.windows().is_empty());
|
||||
|
||||
let json = r#"{
|
||||
"id": "test-2",
|
||||
"windows": { "elements": [ { "hwnd": 5 }, { "hwnd": 9 } ], "focused": 1 }
|
||||
}"#;
|
||||
let container: Container = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(container.id, "test-2");
|
||||
assert!(!container.locked);
|
||||
assert_eq!(container.windows(), &[Window::from(5), Window::from(9)]);
|
||||
assert_eq!(container.focused_window_idx(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_and_deserializes() {
|
||||
let mut container = Container::default();
|
||||
container.set_locked(true);
|
||||
|
||||
let serialized = serde_json::to_string(&container).expect("Should serialize");
|
||||
let deserialized: Container =
|
||||
serde_json::from_str(&serialized).expect("Should deserialize");
|
||||
|
||||
assert!(deserialized.locked);
|
||||
assert_eq!(deserialized.id, container.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ use clap::ValueEnum;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::ser::SerializeSeq;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum AnimationStyle {
|
||||
Linear,
|
||||
@@ -38,4 +39,81 @@ pub enum AnimationStyle {
|
||||
EaseInBounce,
|
||||
EaseOutBounce,
|
||||
EaseInOutBounce,
|
||||
#[value(skip)]
|
||||
CubicBezier(f64, f64, f64, f64),
|
||||
}
|
||||
|
||||
// Custom serde implementation
|
||||
impl<'de> Deserialize<'de> for AnimationStyle {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct AnimationStyleVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for AnimationStyleVisitor {
|
||||
type Value = AnimationStyle;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string or an array of four f64 values")
|
||||
}
|
||||
|
||||
// Handle string variants (e.g., "EaseInOutExpo")
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
value.parse().map_err(|_| E::unknown_variant(value, &[]))
|
||||
}
|
||||
|
||||
// Handle CubicBezier array (e.g., [0.32, 0.72, 0.0, 1.0])
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let x1 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
|
||||
let y1 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
|
||||
let x2 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
|
||||
let y2 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
|
||||
|
||||
// Ensure no extra elements
|
||||
if seq.next_element::<serde::de::IgnoredAny>()?.is_some() {
|
||||
return Err(serde::de::Error::invalid_length(5, &self));
|
||||
}
|
||||
|
||||
Ok(AnimationStyle::CubicBezier(x1, y1, x2, y2))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(AnimationStyleVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AnimationStyle {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
// Serialize CubicBezier as an array
|
||||
AnimationStyle::CubicBezier(x1, y1, x2, y2) => {
|
||||
let mut seq = serializer.serialize_seq(Some(4))?;
|
||||
seq.serialize_element(x1)?;
|
||||
seq.serialize_element(y1)?;
|
||||
seq.serialize_element(x2)?;
|
||||
seq.serialize_element(y2)?;
|
||||
seq.end()
|
||||
}
|
||||
// Serialize all other variants as strings
|
||||
_ => serializer.serialize_str(&self.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use super::custom_layout::Column;
|
||||
use super::custom_layout::ColumnSplit;
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Rect;
|
||||
use super::custom_layout::Column;
|
||||
use super::custom_layout::ColumnSplit;
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
|
||||
pub trait Arrangement {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
@@ -21,6 +23,9 @@ pub trait Arrangement {
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
focused_idx: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
latest_layout: &[Rect],
|
||||
) -> Vec<Rect>;
|
||||
}
|
||||
|
||||
@@ -33,9 +38,99 @@ impl Arrangement for DefaultLayout {
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
focused_idx: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
latest_layout: &[Rect],
|
||||
) -> Vec<Rect> {
|
||||
let len = usize::from(len);
|
||||
let mut dimensions = match self {
|
||||
Self::Scrolling => {
|
||||
let column_count = layout_options
|
||||
.and_then(|o| o.scrolling.map(|s| s.columns))
|
||||
.unwrap_or(3);
|
||||
|
||||
let column_width = area.right / column_count as i32;
|
||||
let mut layouts = Vec::with_capacity(len);
|
||||
|
||||
let visible_columns = area.right / column_width;
|
||||
let keep_centered = layout_options
|
||||
.and_then(|o| {
|
||||
o.scrolling
|
||||
.map(|s| s.center_focused_column.unwrap_or_default())
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let first_visible: isize = if focused_idx == 0 {
|
||||
// if focused idx is 0, we are at the beginning of the scrolling strip
|
||||
0
|
||||
} else {
|
||||
let previous_first_visible = if latest_layout.is_empty() {
|
||||
0
|
||||
} else {
|
||||
// previous first_visible based on the left position of the first visible window
|
||||
let left_edge = area.left;
|
||||
latest_layout
|
||||
.iter()
|
||||
.position(|rect| rect.left >= left_edge)
|
||||
.unwrap_or(0) as isize
|
||||
};
|
||||
|
||||
let focused_idx = focused_idx as isize;
|
||||
|
||||
// if center_focused_column is enabled, and we have an odd number of visible columns,
|
||||
// center the focused window column
|
||||
if keep_centered && visible_columns % 2 == 1 {
|
||||
let center_offset = visible_columns as isize / 2;
|
||||
(focused_idx - center_offset).max(0).min(
|
||||
(len as isize)
|
||||
.saturating_sub(visible_columns as isize)
|
||||
.max(0),
|
||||
)
|
||||
} else {
|
||||
if focused_idx < previous_first_visible {
|
||||
// focused window is off the left edge, we need to scroll left
|
||||
focused_idx
|
||||
} else if focused_idx >= previous_first_visible + visible_columns as isize {
|
||||
// focused window is off the right edge, we need to scroll right
|
||||
// and make sure it's the last visible window
|
||||
(focused_idx + 1 - visible_columns as isize).max(0)
|
||||
} else {
|
||||
// focused window is already visible, we don't need to scroll
|
||||
previous_first_visible
|
||||
}
|
||||
.min(
|
||||
(len as isize)
|
||||
.saturating_sub(visible_columns as isize)
|
||||
.max(0),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
for i in 0..len {
|
||||
let position = (i as isize) - first_visible;
|
||||
let left = area.left + (position as i32 * column_width);
|
||||
|
||||
layouts.push(Rect {
|
||||
left,
|
||||
top: area.top,
|
||||
right: column_width,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::BSP => recursive_fibonacci(
|
||||
0,
|
||||
len,
|
||||
@@ -60,10 +155,9 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
columns_reverse(&mut layouts);
|
||||
}
|
||||
) && let 2.. = len
|
||||
{
|
||||
columns_reverse(&mut layouts);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -85,10 +179,9 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
rows_reverse(&mut layouts);
|
||||
}
|
||||
) && let 2.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -139,25 +232,23 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
) && let 2.. = len
|
||||
{
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left;
|
||||
}
|
||||
primary.left = rest[0].left + rest[0].right;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left;
|
||||
}
|
||||
primary.left = rest[0].left + rest[0].right;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 3.. = len {
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
) && let 3.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -211,25 +302,23 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
) && let 2.. = len
|
||||
{
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
primary.left = rest[0].left;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left + primary.right;
|
||||
}
|
||||
primary.left = rest[0].left;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left + primary.right;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 3.. = len {
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
) && let 3.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -280,25 +369,23 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
) && let 2.. = len
|
||||
{
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
for rect in rest.iter_mut() {
|
||||
rect.top = primary.top;
|
||||
}
|
||||
primary.top = rest[0].top + rest[0].bottom;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.top = primary.top;
|
||||
}
|
||||
primary.top = rest[0].top + rest[0].bottom;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 3.. = len {
|
||||
columns_reverse(&mut layouts[1..]);
|
||||
}
|
||||
) && let 3.. = len
|
||||
{
|
||||
columns_reverse(&mut layouts[1..]);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -407,10 +494,9 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 4.. = len {
|
||||
rows_reverse(&mut layouts[2..]);
|
||||
}
|
||||
) && let 4.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts[2..]);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -428,14 +514,25 @@ impl Arrangement for DefaultLayout {
|
||||
|
||||
let len = len as i32;
|
||||
|
||||
let num_cols = (len as f32).sqrt().ceil() as i32;
|
||||
let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
|
||||
let num_cols = if let Some(rows) = row_constraint {
|
||||
((len as f32) / (rows as f32)).ceil() as i32
|
||||
} else {
|
||||
(len as f32).sqrt().ceil() as i32
|
||||
};
|
||||
|
||||
let mut iter = layouts.iter_mut().enumerate().peekable();
|
||||
|
||||
for col in 0..num_cols {
|
||||
let iter_peek = iter.peek().map(|x| x.0).unwrap_or_default() as i32;
|
||||
let remaining_windows = len - iter_peek;
|
||||
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
|
||||
};
|
||||
|
||||
let win_height = area.bottom / num_rows_in_this_col;
|
||||
let win_width = area.right / num_cols;
|
||||
@@ -487,6 +584,9 @@ impl Arrangement for CustomLayout {
|
||||
container_padding: Option<i32>,
|
||||
_layout_flip: Option<Axis>,
|
||||
_resize_dimensions: &[Option<Rect>],
|
||||
_focused_idx: usize,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
_latest_layout: &[Rect],
|
||||
) -> Vec<Rect> {
|
||||
let mut dimensions = vec![];
|
||||
let container_count = len.get();
|
||||
@@ -541,7 +641,7 @@ impl Arrangement for CustomLayout {
|
||||
};
|
||||
|
||||
match column {
|
||||
Column::Primary(Option::Some(_)) => {
|
||||
Column::Primary(Some(_)) => {
|
||||
let main_column_area = if idx == 0 {
|
||||
Self::main_column_area(area, primary_right, None)
|
||||
} else {
|
||||
@@ -673,67 +773,67 @@ fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Optio
|
||||
|
||||
// This needs to be aware of layout flips
|
||||
for (i, opt) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(resize_ref) = opt {
|
||||
if i > 0 {
|
||||
if resize_ref.left != 0 {
|
||||
#[allow(clippy::if_not_else)]
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 != 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
if let Some(resize_ref) = opt
|
||||
&& i > 0
|
||||
{
|
||||
if resize_ref.left != 0 {
|
||||
#[allow(clippy::if_not_else)]
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 != 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 == 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.right += resize_ref.left;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: resize_ref.left,
|
||||
bottom: 0,
|
||||
});
|
||||
}
|
||||
for n in range {
|
||||
let should_adjust = n % 2 == 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.right += resize_ref.left;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: resize_ref.left,
|
||||
bottom: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||
rr.left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if resize_ref.top != 0 {
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 == 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||
rr.left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 != 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.bottom += resize_ref.top;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: resize_ref.top,
|
||||
});
|
||||
}
|
||||
if resize_ref.top != 0 {
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 == 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 != 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.bottom += resize_ref.top;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: resize_ref.top,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||
resize.top = 0;
|
||||
}
|
||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||
resize.top = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -818,7 +918,7 @@ fn recursive_fibonacci(
|
||||
right: resized.right,
|
||||
bottom: resized.bottom,
|
||||
}]
|
||||
} else if idx % 2 != 0 {
|
||||
} else if !idx.is_multiple_of(2) {
|
||||
let mut res = vec![Rect {
|
||||
left: resized.left,
|
||||
top: main_y,
|
||||
@@ -1115,6 +1215,37 @@ fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rec
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_scrolling_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
|
||||
if len <= 1 {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (i, rect) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
let is_leftmost = i == 0;
|
||||
let is_rightmost = i == len - 1;
|
||||
|
||||
resize_left(&mut result[i], rect.left);
|
||||
resize_right(&mut result[i], rect.right);
|
||||
resize_top(&mut result[i], rect.top);
|
||||
resize_bottom(&mut result[i], rect.bottom);
|
||||
|
||||
if !is_leftmost && rect.left != 0 {
|
||||
resize_right(&mut result[i - 1], rect.left);
|
||||
}
|
||||
|
||||
if !is_rightmost && rect.right != 0 {
|
||||
resize_left(&mut result[i + 1], rect.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn resize_left(rect: &mut Rect, resize: i32) {
|
||||
rect.left += resize / 2;
|
||||
rect.right += -resize / 2;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config_generation::ApplicationConfiguration;
|
||||
use crate::config_generation::ApplicationOptions;
|
||||
use crate::config_generation::MatchingRule;
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -36,12 +36,12 @@ impl DerefMut for 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)?;
|
||||
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)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
@@ -142,11 +142,11 @@ impl ApplicationConfiguration {
|
||||
pub struct 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)?)
|
||||
}
|
||||
|
||||
pub fn format(content: &str) -> Result<String> {
|
||||
pub fn format(content: &str) -> eyre::Result<String> {
|
||||
let mut cfgen = Self::load(content)?;
|
||||
for cfg in &mut cfgen {
|
||||
cfg.populate_default_matching_strategies();
|
||||
@@ -156,7 +156,10 @@ impl ApplicationConfigurationGenerator {
|
||||
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 override_cfgen = Self::load(override_content)?;
|
||||
|
||||
@@ -182,7 +185,7 @@ impl ApplicationConfigurationGenerator {
|
||||
pub fn generate_pwsh(
|
||||
base_content: &str,
|
||||
override_content: Option<&str>,
|
||||
) -> Result<Vec<String>> {
|
||||
) -> eyre::Result<Vec<String>> {
|
||||
let mut cfgen = if let Some(override_content) = override_content {
|
||||
Self::merge(base_content, override_content)?
|
||||
} else {
|
||||
@@ -233,7 +236,10 @@ impl ApplicationConfigurationGenerator {
|
||||
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 {
|
||||
Self::merge(base_content, override_content)?
|
||||
} else {
|
||||
|
||||
@@ -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::fs::File;
|
||||
use std::io::BufReader;
|
||||
@@ -5,12 +9,6 @@ use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
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;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -32,7 +30,7 @@ impl DerefMut for 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 layout: Self = match path.extension() {
|
||||
Some(extension) if extension == "yaml" || extension == "yml" => {
|
||||
@@ -41,7 +39,7 @@ impl CustomLayout {
|
||||
Some(extension) if extension == "json" => {
|
||||
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() {
|
||||
|
||||
@@ -21,9 +21,35 @@ pub enum DefaultLayout {
|
||||
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>,
|
||||
/// Options related to the Grid layout
|
||||
pub grid: Option<GridLayoutOptions>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
/// 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))]
|
||||
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 {
|
||||
@@ -31,6 +57,7 @@ impl DefaultLayout {
|
||||
n if n > 1 => 1,
|
||||
_ => 0,
|
||||
},
|
||||
Self::Scrolling => 0,
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
@@ -53,6 +80,7 @@ impl DefaultLayout {
|
||||
_ => len.saturating_sub(1),
|
||||
},
|
||||
DefaultLayout::RightMainVerticalStack => 0,
|
||||
DefaultLayout::Scrolling => len.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +103,7 @@ impl DefaultLayout {
|
||||
| Self::RightMainVerticalStack
|
||||
| Self::HorizontalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::Scrolling
|
||||
) {
|
||||
return None;
|
||||
};
|
||||
@@ -169,13 +198,15 @@ impl DefaultLayout {
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
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,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::DefaultLayout;
|
||||
use super::OperationDirection;
|
||||
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::OperationDirection;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
|
||||
pub trait Direction {
|
||||
fn index_in_direction(
|
||||
@@ -11,6 +12,7 @@ pub trait Direction {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize>;
|
||||
|
||||
fn is_valid_direction(
|
||||
@@ -18,30 +20,35 @@ pub trait Direction {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool;
|
||||
fn up_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
fn down_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
fn left_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
fn right_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
}
|
||||
|
||||
@@ -51,32 +58,53 @@ impl Direction for DefaultLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize> {
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.left_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.right_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.up_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.down_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -89,6 +117,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool {
|
||||
if count < 2 {
|
||||
return false;
|
||||
@@ -101,16 +130,18 @@ impl Direction for DefaultLayout {
|
||||
Self::Rows | Self::HorizontalStack => idx != 0,
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
||||
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,
|
||||
},
|
||||
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::Rows => idx != count - 1,
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
|
||||
Self::HorizontalStack => idx == 0,
|
||||
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,
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
Self::BSP => idx != 0,
|
||||
@@ -119,10 +150,11 @@ impl Direction for DefaultLayout {
|
||||
Self::Rows => false,
|
||||
Self::HorizontalStack => idx != 0 && 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,
|
||||
},
|
||||
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::Rows => false,
|
||||
Self::VerticalStack => idx == 0,
|
||||
@@ -132,7 +164,8 @@ impl Direction for DefaultLayout {
|
||||
2 => idx != 0,
|
||||
_ => 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -142,10 +175,11 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
if idx.is_multiple_of(2) {
|
||||
idx - 1
|
||||
} else {
|
||||
idx - 2
|
||||
@@ -157,7 +191,8 @@ impl Direction for DefaultLayout {
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::RightMainVerticalStack => idx - 1,
|
||||
Self::HorizontalStack => 0,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +201,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP
|
||||
@@ -175,7 +211,8 @@ impl Direction for DefaultLayout {
|
||||
| Self::RightMainVerticalStack => idx + 1,
|
||||
Self::Columns => unreachable!(),
|
||||
Self::HorizontalStack => 1,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,10 +221,11 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
if idx.is_multiple_of(2) {
|
||||
idx - 2
|
||||
} else {
|
||||
idx - 1
|
||||
@@ -202,7 +240,8 @@ impl Direction for DefaultLayout {
|
||||
1 => unreachable!(),
|
||||
_ => 0,
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => idx - 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +250,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
|
||||
@@ -222,7 +262,8 @@ impl Direction for DefaultLayout {
|
||||
0 => 2,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => idx + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,21 +293,32 @@ struct GridTouchingEdges {
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn get_grid_item(idx: usize, count: usize) -> GridItem {
|
||||
let num_cols = (count as f32).sqrt().ceil() as usize;
|
||||
fn get_grid_item(idx: usize, count: usize, layout_options: Option<LayoutOptions>) -> GridItem {
|
||||
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;
|
||||
|
||||
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 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 {
|
||||
if iter == idx {
|
||||
return GridItem {
|
||||
state: GridItemState::Valid,
|
||||
row: row + 1,
|
||||
num_rows: num_rows_in_this_col,
|
||||
row: (row + 1) as usize,
|
||||
num_rows: num_rows_in_this_col as usize,
|
||||
touching_edges: GridTouchingEdges {
|
||||
left: col == 0,
|
||||
right: col == num_cols - 1,
|
||||
@@ -293,8 +345,13 @@ fn get_grid_item(idx: usize, count: usize) -> GridItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_grid_edge(op_direction: OperationDirection, idx: usize, count: usize) -> bool {
|
||||
let item = get_grid_item(idx, count);
|
||||
fn is_grid_edge(
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool {
|
||||
let item = get_grid_item(idx, count, layout_options);
|
||||
|
||||
match item.state {
|
||||
GridItemState::Invalid => false,
|
||||
@@ -311,6 +368,7 @@ fn grid_neighbor(
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
let Some(op_direction) = op_direction else {
|
||||
return 0;
|
||||
@@ -320,11 +378,11 @@ fn grid_neighbor(
|
||||
return 0;
|
||||
};
|
||||
|
||||
let item = get_grid_item(idx, count);
|
||||
let item = get_grid_item(idx, count, layout_options);
|
||||
|
||||
match op_direction {
|
||||
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 {
|
||||
return idx - (item.num_rows - 1);
|
||||
@@ -348,36 +406,42 @@ impl Direction for CustomLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize> {
|
||||
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 {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.left_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.right_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.up_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.down_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -390,9 +454,15 @@ impl Direction for CustomLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool {
|
||||
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 {
|
||||
@@ -436,6 +506,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
idx - 1
|
||||
}
|
||||
@@ -445,6 +516,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
idx + 1
|
||||
}
|
||||
@@ -454,6 +526,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
if column_idx - 1 == 0 {
|
||||
@@ -468,6 +541,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
self.first_container_idx(column_idx + 1)
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
use crate::KomorebiTheme;
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
@@ -22,14 +23,14 @@ 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 default_layout::*;
|
||||
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::ResolvedPathBuf;
|
||||
pub use pathext::replace_env_in_path;
|
||||
pub use pathext::resolve_option_hashmap_usize_path;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
@@ -54,6 +55,8 @@ pub enum SocketMessage {
|
||||
// Window / Container Commands
|
||||
FocusWindow(OperationDirection),
|
||||
MoveWindow(OperationDirection),
|
||||
PreselectDirection(OperationDirection),
|
||||
CancelPreselect,
|
||||
CycleFocusWindow(CycleDirection),
|
||||
CycleMoveWindow(CycleDirection),
|
||||
StackWindow(OperationDirection),
|
||||
@@ -86,6 +89,7 @@ pub enum SocketMessage {
|
||||
Close,
|
||||
Minimize,
|
||||
Promote,
|
||||
PromoteSwap,
|
||||
PromoteFocus,
|
||||
PromoteWindow(OperationDirection),
|
||||
EagerFocus(String),
|
||||
@@ -108,6 +112,7 @@ pub enum SocketMessage {
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(DefaultLayout),
|
||||
CycleLayout(CycleDirection),
|
||||
ScrollingLayoutColumns(NonZeroUsize),
|
||||
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
FlipLayout(Axis),
|
||||
ToggleWorkspaceWindowContainerBehaviour,
|
||||
@@ -200,6 +205,7 @@ pub enum SocketMessage {
|
||||
StackbarFontFamily(Option<String>),
|
||||
WorkAreaOffset(Rect),
|
||||
MonitorWorkAreaOffset(usize, Rect),
|
||||
WorkspaceWorkAreaOffset(usize, usize, Rect),
|
||||
ToggleWindowBasedWorkAreaOffset,
|
||||
ResizeDelta(i32),
|
||||
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
@@ -245,7 +251,7 @@ pub enum 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())
|
||||
}
|
||||
}
|
||||
@@ -253,7 +259,7 @@ impl SocketMessage {
|
||||
impl FromStr for SocketMessage {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -341,6 +347,7 @@ pub enum StateQuery {
|
||||
FocusedWindowIndex,
|
||||
FocusedWorkspaceName,
|
||||
FocusedWorkspaceLayout,
|
||||
FocusedContainerKind,
|
||||
Version,
|
||||
}
|
||||
|
||||
@@ -377,6 +384,21 @@ pub struct WindowManagementBehaviour {
|
||||
/// that can be later toggled to tiled, when false it will default to
|
||||
/// `current_behaviour` again.
|
||||
pub float_override: bool,
|
||||
/// Determines if a new window should be spawned floating when on the floating layer and the
|
||||
/// floating layer behaviour is set to float. This value is always calculated when checking for
|
||||
/// the management behaviour on a specific workspace.
|
||||
pub floating_layer_override: bool,
|
||||
/// The floating layer behaviour to be used if the float override is being used
|
||||
pub floating_layer_behaviour: FloatingLayerBehaviour,
|
||||
/// The `Placement` to be used when toggling a window to float
|
||||
pub toggle_float_placement: Placement,
|
||||
/// The `Placement` to be used when spawning a window on the floating layer with the
|
||||
/// `FloatingLayerBehaviour` set to `FloatingLayerBehaviour::Float`
|
||||
pub floating_layer_placement: Placement,
|
||||
/// The `Placement` to be used when spawning a window with float override active
|
||||
pub float_override_placement: Placement,
|
||||
/// The `Placement` to be used when spawning a window that matches a 'floating_applications' rule
|
||||
pub float_rule_placement: Placement,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -396,17 +418,59 @@ pub enum WindowContainerBehaviour {
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum FloatingLayerBehaviour {
|
||||
/// Tile new windows (unless they match a float rule)
|
||||
/// Tile new windows (unless they match a float rule or float override is active)
|
||||
#[default]
|
||||
Tile,
|
||||
/// Float new windows
|
||||
Float,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum Placement {
|
||||
/// Does not change the size or position of the window
|
||||
#[default]
|
||||
None,
|
||||
/// Center the window without changing the size
|
||||
Center,
|
||||
/// Center the window and resize it according to the `AspectRatio`
|
||||
CenterAndResize,
|
||||
}
|
||||
|
||||
impl FloatingLayerBehaviour {
|
||||
pub fn should_float(&self) -> bool {
|
||||
match self {
|
||||
FloatingLayerBehaviour::Tile => false,
|
||||
FloatingLayerBehaviour::Float => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
pub fn should_center(&self) -> bool {
|
||||
match self {
|
||||
Placement::None => false,
|
||||
Placement::Center | Placement::CenterAndResize => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_resize(&self) -> bool {
|
||||
match self {
|
||||
Placement::None | Placement::Center => false,
|
||||
Placement::CenterAndResize => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum MoveBehaviour {
|
||||
/// Swap the window container with the window container at the edge of the adjacent monitor
|
||||
#[default]
|
||||
Swap,
|
||||
/// Insert the window container into the focused workspace on the adjacent monitor
|
||||
Insert,
|
||||
@@ -414,19 +478,22 @@ pub enum MoveBehaviour {
|
||||
NoOp,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum CrossBoundaryBehaviour {
|
||||
/// Attempt to perform actions across a workspace boundary
|
||||
Workspace,
|
||||
/// Attempt to perform actions across a monitor boundary
|
||||
#[default]
|
||||
Monitor,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum HidingBehaviour {
|
||||
/// 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)
|
||||
Hide,
|
||||
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
Minimize,
|
||||
@@ -434,10 +501,13 @@ pub enum HidingBehaviour {
|
||||
Cloak,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum OperationBehaviour {
|
||||
/// Process komorebic commands on temporarily unmanaged/floated windows
|
||||
#[default]
|
||||
Op,
|
||||
/// Ignore komorebic commands on temporarily unmanaged/floated windows
|
||||
NoOp,
|
||||
@@ -466,6 +536,16 @@ impl Sizing {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum WindowHandlingBehaviour {
|
||||
#[default]
|
||||
Sync,
|
||||
Async,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -473,7 +553,9 @@ mod tests {
|
||||
#[test]
|
||||
fn deserializes() {
|
||||
// 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 message: SocketMessage = serde_json::from_str(json).unwrap();
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use super::Axis;
|
||||
use super::direction::Direction;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use clap::ValueEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use super::direction::Direction;
|
||||
use super::Axis;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum OperationDirection {
|
||||
@@ -57,7 +57,8 @@ impl OperationDirection {
|
||||
layout_flip: Option<Axis>,
|
||||
idx: usize,
|
||||
len: NonZeroUsize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> 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,5 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
@@ -10,9 +9,9 @@ use serde::Serialize;
|
||||
|
||||
/// Path extension trait
|
||||
pub trait PathExt {
|
||||
/// Resolve environment variables components in a path.
|
||||
/// Resolve environment variable components in a path.
|
||||
///
|
||||
/// Resolves the follwing formats:
|
||||
/// Resolves the following formats:
|
||||
/// - CMD: `%variable%`
|
||||
/// - PowerShell: `$Env:variable`
|
||||
/// - Bash: `$variable`.
|
||||
@@ -58,7 +57,7 @@ impl<P: AsRef<Path>> PathExt for P {
|
||||
// if component is a variable, get the value from the environment
|
||||
if let Some(var) = 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);
|
||||
continue;
|
||||
}
|
||||
@@ -127,8 +126,8 @@ impl serde_with::schemars_0_8::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
|
||||
"PathBuf".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
<PathBuf as schemars::JsonSchema>::json_schema(gen)
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::schema::Schema {
|
||||
<PathBuf as schemars::JsonSchema>::json_schema(generator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +164,9 @@ mod tests {
|
||||
#[test]
|
||||
fn resolves_env_vars() {
|
||||
// Set a variable for testing
|
||||
std::env::set_var("VAR", "VALUE");
|
||||
unsafe {
|
||||
std::env::set_var("VAR", "VALUE");
|
||||
}
|
||||
|
||||
// %VAR% format
|
||||
assert_eq!(resolve("/path/%VAR%/d"), expected("/path/VALUE/d"));
|
||||
@@ -183,7 +184,9 @@ mod tests {
|
||||
assert_eq!(resolve("/path/$ASD/to/d"), expected("/path/$ASD/to/d"));
|
||||
|
||||
// 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
|
||||
assert_eq!(resolve("~"), expected("C:\\Users\\user"));
|
||||
|
||||
@@ -41,6 +41,10 @@ impl Rect {
|
||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||
self.right == rhs.right && self.bottom == rhs.bottom
|
||||
}
|
||||
|
||||
pub fn has_same_position_as(&self, rhs: &Self) -> bool {
|
||||
self.left == rhs.left && self.top == rhs.top
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
|
||||
@@ -44,13 +44,15 @@ pub fn send_notification(hwnd: isize) {
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ pub mod ring;
|
||||
pub mod container;
|
||||
pub mod core;
|
||||
pub mod focus_manager;
|
||||
pub mod locked_deque;
|
||||
pub mod lockable_sequence;
|
||||
pub mod monitor;
|
||||
pub mod monitor_reconciliator;
|
||||
pub mod process_command;
|
||||
@@ -16,7 +16,9 @@ pub mod process_event;
|
||||
pub mod process_movement;
|
||||
pub mod reaper;
|
||||
pub mod set_window_position;
|
||||
pub mod splash;
|
||||
pub mod stackbar_manager;
|
||||
pub mod state;
|
||||
pub mod static_config;
|
||||
pub mod styles;
|
||||
pub mod theme_manager;
|
||||
@@ -39,12 +41,12 @@ use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use core::*;
|
||||
pub use komorebi_themes::colour::*;
|
||||
@@ -62,17 +64,19 @@ use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use crate::core::config_generation::WorkspaceMatchingRule;
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use os_info::Version;
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::RwLock;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use state::State;
|
||||
use uds_windows::UnixStream;
|
||||
use which::which;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
|
||||
lazy_static! {
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
@@ -159,7 +163,14 @@ lazy_static! {
|
||||
})
|
||||
]));
|
||||
static ref SESSION_FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("komorebi-shortcuts.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
|
||||
]));
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
@@ -192,8 +203,7 @@ lazy_static! {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
|
||||
@@ -204,11 +214,10 @@ lazy_static! {
|
||||
pub static ref AHK_EXE: String = {
|
||||
let mut ahk: String = String::from("autohotkey.exe");
|
||||
|
||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
|
||||
if which(&komorebi_ahk_exe).is_ok() {
|
||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE")
|
||||
&& which(&komorebi_ahk_exe).is_ok() {
|
||||
ahk = komorebi_ahk_exe;
|
||||
}
|
||||
}
|
||||
|
||||
ahk
|
||||
};
|
||||
@@ -227,6 +236,8 @@ lazy_static! {
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
static ref FLOATING_WINDOW_TOGGLE_ASPECT_RATIO: Arc<Mutex<AspectRatio>> = Arc::new(Mutex::new(AspectRatio::Predefined(PredefinedAspectRatio::Widescreen)));
|
||||
|
||||
static ref CURRENT_VIRTUAL_DESKTOP: Arc<Mutex<Option<Vec<u8>>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
@@ -240,8 +251,34 @@ pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub static SLOW_APPLICATION_COMPENSATION_TIME: AtomicU64 = AtomicU64::new(20);
|
||||
|
||||
pub static WINDOW_HANDLING_BEHAVIOUR: AtomicCell<WindowHandlingBehaviour> =
|
||||
AtomicCell::new(WindowHandlingBehaviour::Sync);
|
||||
|
||||
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.
|
||||
pub trait Lockable {
|
||||
/// Returns `true` if the item is locked.
|
||||
fn locked(&self) -> bool;
|
||||
/// Sets the locked state of the item.
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
@@ -286,23 +323,34 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
current
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
Monitor(MonitorNotification),
|
||||
VirtualDesktop(VirtualDesktopNotification),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum VirtualDesktopNotification {
|
||||
EnteredAssociatedVirtualDesktop,
|
||||
LeftAssociatedVirtualDesktop,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
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!(
|
||||
notification.event,
|
||||
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
|
||||
@@ -383,7 +431,7 @@ pub fn notify_subscribers(notification: Notification, state_has_been_modified: b
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_configuration() -> Result<()> {
|
||||
pub fn load_configuration() -> eyre::Result<()> {
|
||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
|
||||
|
||||
357
komorebi/src/lockable_sequence.rs
Normal file
357
komorebi/src/lockable_sequence.rs
Normal file
@@ -0,0 +1,357 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::Lockable;
|
||||
|
||||
/// A sequence supporting insertion, removal, and swapping of elements while preserving the absolute
|
||||
/// positions of locked items.
|
||||
pub trait LockableSequence<T: Lockable> {
|
||||
/// Inserts a value at `idx`, keeping locked elements at their absolute positions.
|
||||
fn insert_respecting_locks(&mut self, idx: usize, value: T) -> usize;
|
||||
/// Removes the element at `idx`, keeping locked elements at their absolute positions.
|
||||
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T>;
|
||||
/// Swaps the elements at indices `i` and `j`, keeping locked elements at their absolute positions.
|
||||
fn swap_respecting_locks(&mut self, i: usize, j: usize);
|
||||
}
|
||||
|
||||
impl<T: Lockable> LockableSequence<T> for VecDeque<T> {
|
||||
/// Insert `value` at logical index `idx`, with trying to keep locked elements
|
||||
/// (`is_locked()`) anchored at their original positions.
|
||||
///
|
||||
/// Returns the final index of the inserted element.
|
||||
fn insert_respecting_locks(&mut self, mut idx: usize, value: T) -> usize {
|
||||
// 1. Bounds check: if index is out of range, simply append.
|
||||
if idx >= self.len() {
|
||||
self.push_back(value);
|
||||
return self.len() - 1; // last index
|
||||
}
|
||||
|
||||
// 2. Normal VecDeque insertion
|
||||
self.insert(idx, value);
|
||||
|
||||
// 3. Walk left-to-right once, swapping any misplaced locked element. After
|
||||
// the VecDeque::insert all items after `idx` have moved right by one. For every locked
|
||||
// element that is now to the right of an unlocked one, swap it back left exactly once.
|
||||
for index in (idx + 1)..self.len() {
|
||||
if self[index].locked() && !self[index - 1].locked() {
|
||||
self.swap(index - 1, index);
|
||||
|
||||
// If the element we just inserted participated in the swap,
|
||||
// update `idx` so we can return its final location.
|
||||
if idx == index - 1 {
|
||||
idx = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
/// Remove element at `idx`, with trying to keep locked elements
|
||||
/// (`is_locked()`) anchored at their original positions.
|
||||
///
|
||||
/// Returns the removed element, or `None` if `idx` is out of bounds.
|
||||
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T> {
|
||||
// 1. Bounds check: if index is out of range, do nothing.
|
||||
if idx >= self.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Remove the element at the requested index.
|
||||
// All elements after idx are now shifted left by 1.
|
||||
let removed = self.remove(idx)?;
|
||||
|
||||
// 3. If less than 2 elements remain, nothing to shift.
|
||||
if self.len() < 2 {
|
||||
return Some(removed);
|
||||
}
|
||||
|
||||
// 4. Iterate from the element just after the removed spot up to the second-to-last
|
||||
// element, right-to-left. This loop "fixes" locked elements that were shifted left
|
||||
// off their anchored positions: If a locked element now has an unlocked element
|
||||
// to its right, swap them back to restore locked order.
|
||||
for index in (idx..self.len() - 1).rev() {
|
||||
// If current is locked and the next one is not locked, swap them.
|
||||
if self[index].locked() && !self[index + 1].locked() {
|
||||
self.swap(index, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Return the removed value.
|
||||
Some(removed)
|
||||
}
|
||||
|
||||
/// Swaps the elements at indices `i` and `j`, along with their `locked` status, ensuring
|
||||
/// the lock state remains associated with the position rather than the element itself.
|
||||
fn swap_respecting_locks(&mut self, i: usize, j: usize) {
|
||||
self.swap(i, j);
|
||||
let locked_i = self[i].locked();
|
||||
let locked_j = self[j].locked();
|
||||
self[i].set_locked(locked_j);
|
||||
self[j].set_locked(locked_i);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct TestItem {
|
||||
val: i32,
|
||||
locked: bool,
|
||||
}
|
||||
|
||||
impl Lockable for TestItem {
|
||||
fn locked(&self) -> bool {
|
||||
self.locked
|
||||
}
|
||||
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self {
|
||||
self.locked = locked;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn vals(v: &VecDeque<TestItem>) -> Vec<i32> {
|
||||
v.iter().map(|x| x.val).collect()
|
||||
}
|
||||
|
||||
fn test_deque(items: &[(i32, bool)]) -> VecDeque<TestItem> {
|
||||
items
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|(val, locked)| TestItem { val, locked })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_respecting_locks() {
|
||||
// Test case 1: Basic insertion with locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
// Insert at index 0, should shift elements while keeping index 2 locked
|
||||
ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
|
||||
assert_eq!(vals(&ring), vec![99, 0, 2, 1, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 2: Insert at a locked index (should insert after locked)
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
// Try to insert at locked index 2, should insert at index 3 instead
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
2,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 3);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 99, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 3: Multiple locked indices
|
||||
{
|
||||
// Lock index 1 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
|
||||
// Insert at index 0, should maintain locked indices
|
||||
ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Elements '1' and '3' remain at indices 1 and 3
|
||||
assert_eq!(vals(&ring), vec![99, 1, 0, 3, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 4: Insert at end
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
5,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
|
||||
}
|
||||
|
||||
// Test case 5: Empty ring
|
||||
{
|
||||
let mut ring = test_deque(&[]);
|
||||
// Insert into empty deque
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 0);
|
||||
assert_eq!(vals(&ring), vec![99]);
|
||||
}
|
||||
|
||||
// Test case 6: All indices locked
|
||||
{
|
||||
// Lock all indices
|
||||
let mut ring = test_deque(&[(0, true), (1, true), (2, true), (3, true), (4, true)]);
|
||||
// Try to insert at index 2, should insert at the end
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
2,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
|
||||
}
|
||||
|
||||
// Test case 7: Consecutive locked indices
|
||||
{
|
||||
// Lock index 2 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, true), (4, false)]);
|
||||
// Insert at index 1, should maintain consecutive locked indices
|
||||
ring.insert_respecting_locks(
|
||||
1,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Elements '2' and '3' remain at indices 2 and 3
|
||||
assert_eq!(vals(&ring), vec![0, 99, 2, 3, 1, 4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_respecting_locks() {
|
||||
// Test case 1: Remove a non-locked index before a locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(0);
|
||||
assert_eq!(removed.map(|x| x.val), Some(0));
|
||||
// Elements '2' remain at index 2
|
||||
assert_eq!(vals(&ring), vec![1, 3, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 2: Remove a locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(2);
|
||||
assert_eq!(removed.map(|x| x.val), Some(2));
|
||||
// Elements should stay at the same places
|
||||
assert_eq!(vals(&ring), vec![0, 1, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 3: Remove an index after a locked index
|
||||
{
|
||||
// Lock index 1
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(3);
|
||||
assert_eq!(removed.map(|x| x.val), Some(3));
|
||||
// Elements should stay at the same places
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 4: Multiple locked indices
|
||||
{
|
||||
// Lock index 1 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(0);
|
||||
assert_eq!(removed.map(|x| x.val), Some(0));
|
||||
// Elements '1' and '3' remain at indices '1' and '3'
|
||||
assert_eq!(vals(&ring), vec![2, 1, 4, 3]);
|
||||
}
|
||||
|
||||
// Test case 5: Remove the last element
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(4);
|
||||
assert_eq!(removed.map(|x| x.val), Some(4));
|
||||
// Index 2 should still be at the same place
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
|
||||
}
|
||||
|
||||
// Test case 6: Invalid index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(10);
|
||||
assert_eq!(removed, None);
|
||||
// Deque unchanged
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 7: Remove enough elements to make a locked index invalid
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true)]);
|
||||
ring.remove_respecting_locks(0);
|
||||
// Index 2 should now be '1'
|
||||
assert_eq!(vals(&ring), vec![1, 2]);
|
||||
}
|
||||
|
||||
// Test case 8: Removing an element before multiple locked indices
|
||||
{
|
||||
// Lock index 2 and 4
|
||||
let mut ring = test_deque(&[
|
||||
(0, false),
|
||||
(1, false),
|
||||
(2, true),
|
||||
(3, false),
|
||||
(4, true),
|
||||
(5, false),
|
||||
]);
|
||||
let removed = ring.remove_respecting_locks(1);
|
||||
assert_eq!(removed.map(|x| x.val), Some(1));
|
||||
// Both indices should still be at the same place
|
||||
assert_eq!(vals(&ring), vec![0, 3, 2, 5, 4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swap_respecting_locks_various_cases() {
|
||||
// Swap unlocked and locked
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false)]);
|
||||
ring.swap_respecting_locks(0, 1);
|
||||
assert_eq!(vals(&ring), vec![1, 0, 2, 3]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
ring.swap_respecting_locks(0, 1);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
|
||||
// Both locked
|
||||
let mut ring = test_deque(&[(0, true), (1, false), (2, true)]);
|
||||
ring.swap_respecting_locks(0, 2);
|
||||
assert_eq!(vals(&ring), vec![2, 1, 0]);
|
||||
assert!(ring[0].locked);
|
||||
assert!(!ring[1].locked);
|
||||
assert!(ring[2].locked);
|
||||
|
||||
// Both unlocked
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false)]);
|
||||
ring.swap_respecting_locks(0, 2);
|
||||
assert_eq!(vals(&ring), vec![2, 1, 0]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
assert!(!ring[2].locked);
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct LockedDeque<'a, T> {
|
||||
deque: &'a mut VecDeque<T>,
|
||||
locked_indices: &'a mut BTreeSet<usize>,
|
||||
}
|
||||
|
||||
impl<'a, T: PartialEq> LockedDeque<'a, T> {
|
||||
pub fn new(deque: &'a mut VecDeque<T>, locked_indices: &'a mut BTreeSet<usize>) -> Self {
|
||||
Self {
|
||||
deque,
|
||||
locked_indices,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, index: usize, value: T) -> usize {
|
||||
insert_respecting_locks(self.deque, self.locked_indices, index, value)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, index: usize) -> Option<T> {
|
||||
remove_respecting_locks(self.deque, self.locked_indices, index)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_respecting_locks<T>(
|
||||
deque: &mut VecDeque<T>,
|
||||
locked_idx: &mut BTreeSet<usize>,
|
||||
idx: usize,
|
||||
value: T,
|
||||
) -> usize {
|
||||
if idx == deque.len() {
|
||||
deque.push_back(value);
|
||||
return idx;
|
||||
}
|
||||
|
||||
let mut new_deque = VecDeque::with_capacity(deque.len() + 1);
|
||||
let mut temp_locked_deque = VecDeque::new();
|
||||
let mut j = 0;
|
||||
let mut corrected_idx = idx;
|
||||
|
||||
for (i, el) in deque.drain(..).enumerate() {
|
||||
if i == idx {
|
||||
corrected_idx = j;
|
||||
}
|
||||
if locked_idx.contains(&i) {
|
||||
temp_locked_deque.push_back(el);
|
||||
} else {
|
||||
new_deque.push_back(el);
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
|
||||
new_deque.insert(corrected_idx, value);
|
||||
|
||||
for (locked_el, locked_idx) in temp_locked_deque.into_iter().zip(locked_idx.iter()) {
|
||||
new_deque.insert(*locked_idx, locked_el);
|
||||
if *locked_idx <= corrected_idx {
|
||||
corrected_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
*deque = new_deque;
|
||||
|
||||
corrected_idx
|
||||
}
|
||||
|
||||
pub fn remove_respecting_locks<T>(
|
||||
deque: &mut VecDeque<T>,
|
||||
locked_idx: &mut BTreeSet<usize>,
|
||||
idx: usize,
|
||||
) -> Option<T> {
|
||||
if idx >= deque.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let final_size = deque.len() - 1;
|
||||
|
||||
let mut new_deque = VecDeque::with_capacity(final_size);
|
||||
let mut temp_locked_deque = VecDeque::new();
|
||||
let mut removed = None;
|
||||
let mut removed_locked_idx = None;
|
||||
|
||||
for (i, el) in deque.drain(..).enumerate() {
|
||||
if i == idx {
|
||||
removed = Some(el);
|
||||
removed_locked_idx = locked_idx.contains(&i).then_some(i);
|
||||
} else if locked_idx.contains(&i) {
|
||||
temp_locked_deque.push_back(el);
|
||||
} else {
|
||||
new_deque.push_back(el);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(i) = removed_locked_idx {
|
||||
let mut above = locked_idx.split_off(&i);
|
||||
above.pop_first();
|
||||
locked_idx.extend(above.into_iter().map(|i| i - 1));
|
||||
}
|
||||
|
||||
while locked_idx.last().is_some_and(|i| *i >= final_size) {
|
||||
locked_idx.pop_last();
|
||||
}
|
||||
|
||||
let extra_invalid_idx = (new_deque.len()
|
||||
..(new_deque.len() + temp_locked_deque.len() - locked_idx.len()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (locked_el, locked_idx) in temp_locked_deque
|
||||
.into_iter()
|
||||
.zip(locked_idx.iter().chain(extra_invalid_idx.iter()))
|
||||
{
|
||||
new_deque.insert(*locked_idx, locked_el);
|
||||
}
|
||||
|
||||
*deque = new_deque;
|
||||
|
||||
removed
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[test]
|
||||
fn test_insert_respecting_locks() {
|
||||
// Test case 1: Basic insertion with locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
// Insert at index 0, should shift elements while keeping index 2 locked
|
||||
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||
assert_eq!(deque, VecDeque::from(vec![99, 0, 2, 1, 3, 4]));
|
||||
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
|
||||
}
|
||||
|
||||
// Test case 2: Insert at a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
// Try to insert at locked index 2, should insert at index 3 instead
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
|
||||
assert_eq!(actual_index, 3);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 99, 3, 4]));
|
||||
}
|
||||
|
||||
// Test case 3: Multiple locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(1); // Lock index 1
|
||||
locked.insert(3); // Lock index 3
|
||||
|
||||
// Insert at index 0, should maintain locked indices
|
||||
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||
assert_eq!(deque, VecDeque::from(vec![99, 1, 0, 3, 2, 4]));
|
||||
// Elements '1' and '3' remain at indices 1 and 3
|
||||
}
|
||||
|
||||
// Test case 4: Insert at end
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
// Insert at end of deque
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 5, 99);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
|
||||
}
|
||||
|
||||
// Test case 5: Empty deque
|
||||
{
|
||||
let mut deque = VecDeque::new();
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// Insert into empty deque
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||
assert_eq!(actual_index, 0);
|
||||
assert_eq!(deque, VecDeque::from(vec![99]));
|
||||
}
|
||||
|
||||
// Test case 6: All indices locked
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
for i in 0..5 {
|
||||
locked.insert(i); // Lock all indices
|
||||
}
|
||||
|
||||
// Try to insert at index 2, should insert at the end
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
|
||||
}
|
||||
|
||||
// Test case 7: Consecutive locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
locked.insert(3); // Lock index 3
|
||||
|
||||
// Insert at index 1, should maintain consecutive locked indices
|
||||
insert_respecting_locks(&mut deque, &mut locked, 1, 99);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 99, 2, 3, 1, 4]));
|
||||
// Elements '2' and '3' remain at indices 2 and 3
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_respecting_locks() {
|
||||
// Test case 1: Remove a non-locked index before a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||
assert_eq!(removed, Some(0));
|
||||
assert_eq!(deque, VecDeque::from(vec![1, 3, 2, 4]));
|
||||
assert!(locked.contains(&2)); // Index 2 should still be locked
|
||||
}
|
||||
|
||||
// Test case 2: Remove a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 2);
|
||||
assert_eq!(removed, Some(2));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 3, 4]));
|
||||
assert!(!locked.contains(&2)); // Index 2 should be unlocked
|
||||
}
|
||||
|
||||
// Test case 3: Remove an index after a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(1); // Lock index 1
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 3);
|
||||
assert_eq!(removed, Some(3));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 4]));
|
||||
assert!(locked.contains(&1)); // Index 1 should still be locked
|
||||
}
|
||||
|
||||
// Test case 4: Multiple locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(1); // Lock index 1
|
||||
locked.insert(3); // Lock index 3
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||
assert_eq!(removed, Some(0));
|
||||
assert_eq!(deque, VecDeque::from(vec![2, 1, 4, 3]));
|
||||
assert!(locked.contains(&1) && locked.contains(&3)); // Both indices should still be locked
|
||||
}
|
||||
|
||||
// Test case 5: Remove the last element
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 4);
|
||||
assert_eq!(removed, Some(4));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3]));
|
||||
assert!(locked.contains(&2)); // Index 2 should still be locked
|
||||
}
|
||||
|
||||
// Test case 6: Invalid index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 10);
|
||||
assert_eq!(removed, None);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4])); // Deque unchanged
|
||||
assert!(locked.contains(&2)); // Lock unchanged
|
||||
}
|
||||
|
||||
// Test case 7: Remove enough elements to make a locked index invalid
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||
assert_eq!(deque, VecDeque::from(vec![1, 2]));
|
||||
assert!(!locked.contains(&2)); // Index 2 should now be invalid
|
||||
}
|
||||
|
||||
// Test case 8: Removing an element before multiple locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4, 5]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
locked.insert(4); // Lock index 4
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 1);
|
||||
assert_eq!(removed, Some(1));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 3, 2, 5, 4]));
|
||||
assert!(locked.contains(&2) && locked.contains(&4)); // Both indices should still be locked
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,30 +10,36 @@
|
||||
use std::env::temp_dir;
|
||||
use std::net::Shutdown;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::bail;
|
||||
use crossbeam_utils::Backoff;
|
||||
use komorebi::animation::AnimationEngine;
|
||||
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
use komorebi::animation::AnimationEngine;
|
||||
use komorebi::replace_env_in_path;
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use sysinfo::Process;
|
||||
use sysinfo::ProcessesToUpdate;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
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::focus_manager;
|
||||
use komorebi::load_configuration;
|
||||
@@ -44,37 +50,36 @@ use komorebi::process_event::listen_for_events;
|
||||
use komorebi::process_movement::listen_for_movements;
|
||||
use komorebi::reaper;
|
||||
use komorebi::stackbar_manager;
|
||||
use komorebi::state::State;
|
||||
use komorebi::static_config::StaticConfig;
|
||||
use komorebi::theme_manager;
|
||||
use komorebi::transparency_manager;
|
||||
use komorebi::window_manager::State;
|
||||
use komorebi::window_manager::WindowManager;
|
||||
use komorebi::windows_api::WindowsApi;
|
||||
use komorebi::winevent_listener;
|
||||
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() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
}
|
||||
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
match log_level {
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Trace => "trace",
|
||||
},
|
||||
);
|
||||
unsafe {
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
match log_level {
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Trace => "trace",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
|
||||
@@ -133,20 +138,22 @@ fn setup(log_level: LogLevel) -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
#[tracing::instrument]
|
||||
fn detect_deadlocks() {
|
||||
// Create a background thread which checks for deadlocks every 10s
|
||||
std::thread::spawn(move || loop {
|
||||
tracing::info!("running deadlock detector");
|
||||
std::thread::sleep(Duration::from_secs(5));
|
||||
let deadlocks = deadlock::check_deadlock();
|
||||
if deadlocks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
tracing::info!("running deadlock detector");
|
||||
std::thread::sleep(Duration::from_secs(5));
|
||||
let deadlocks = deadlock::check_deadlock();
|
||||
if deadlocks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::error!("{} deadlocks detected", deadlocks.len());
|
||||
for (i, threads) in deadlocks.iter().enumerate() {
|
||||
tracing::error!("deadlock #{}", i);
|
||||
for t in threads {
|
||||
tracing::error!("thread id: {:#?}", t.thread_id());
|
||||
tracing::error!("{:#?}", t.backtrace());
|
||||
tracing::error!("{} deadlocks detected", deadlocks.len());
|
||||
for (i, threads) in deadlocks.iter().enumerate() {
|
||||
tracing::error!("deadlock #{}", i);
|
||||
for t in threads {
|
||||
tracing::error!("thread id: {:#?}", t.thread_id());
|
||||
tracing::error!("{:#?}", t.backtrace());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -189,12 +196,30 @@ struct Opts {
|
||||
|
||||
#[tracing::instrument]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn main() -> Result<()> {
|
||||
fn main() -> eyre::Result<()> {
|
||||
let opts: Opts = Opts::parse();
|
||||
CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst);
|
||||
|
||||
let mut set_foreground_window_retries = 5;
|
||||
let mut set_foreground_window_succeeded = false;
|
||||
|
||||
let process_id = WindowsApi::current_process_id();
|
||||
WindowsApi::allow_set_foreground_window(process_id)?;
|
||||
while set_foreground_window_retries > 0 && !set_foreground_window_succeeded {
|
||||
match WindowsApi::allow_set_foreground_window(process_id) {
|
||||
Ok(_) => {
|
||||
set_foreground_window_succeeded = true;
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
set_foreground_window_retries -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if set_foreground_window_retries == 0 {
|
||||
bail!("failed call to AllowSetForegroundWindow after 5 retries");
|
||||
}
|
||||
}
|
||||
|
||||
WindowsApi::set_process_dpi_awareness_context()?;
|
||||
|
||||
let session_id = WindowsApi::process_id_to_session_id()?;
|
||||
@@ -208,15 +233,17 @@ fn main() -> Result<()> {
|
||||
if matched_procs.len() > 1 {
|
||||
let mut len = matched_procs.len();
|
||||
for proc in matched_procs {
|
||||
if let Some(executable_path) = proc.exe() {
|
||||
if executable_path.to_string_lossy().contains("shims") {
|
||||
len -= 1;
|
||||
}
|
||||
if let Some(executable_path) = proc.exe()
|
||||
&& executable_path.to_string_lossy().contains("shims")
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,9 @@ use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
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::Result;
|
||||
use getset::CopyGetters;
|
||||
use getset::Getters;
|
||||
use getset::MutGetters;
|
||||
use getset::Setters;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -17,56 +13,41 @@ use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::core::Rect;
|
||||
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::DefaultLayout;
|
||||
use crate::FloatingLayerBehaviour;
|
||||
use crate::Layout;
|
||||
use crate::OperationDirection;
|
||||
use crate::Wallpaper;
|
||||
use crate::WindowsApi;
|
||||
use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::workspace::WorkspaceGlobals;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Layout;
|
||||
use crate::OperationDirection;
|
||||
use crate::Wallpaper;
|
||||
use crate::WindowsApi;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, PartialEq,
|
||||
)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Monitor {
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub id: isize,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub name: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub device: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub device_id: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub serial_number_id: Option<String>,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub size: Rect,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub work_area_size: Rect,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub work_area_offset: Option<Rect>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub window_based_work_area_offset: Option<Rect>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub window_based_work_area_offset_limit: isize,
|
||||
pub workspaces: Ring<Workspace>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub last_focused_workspace: Option<usize>,
|
||||
#[getset(get_mut = "pub")]
|
||||
pub workspace_names: HashMap<usize, String>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub container_padding: Option<i32>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub workspace_padding: Option<i32>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub wallpaper: Option<Wallpaper>,
|
||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||
}
|
||||
|
||||
impl_ring_elements!(Monitor, Workspace);
|
||||
@@ -123,6 +104,7 @@ pub fn new(
|
||||
container_padding: None,
|
||||
workspace_padding: None,
|
||||
wallpaper: None,
|
||||
floating_layer_behaviour: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,28 +147,29 @@ impl Monitor {
|
||||
container_padding: None,
|
||||
workspace_padding: None,
|
||||
wallpaper: None,
|
||||
floating_layer_behaviour: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused_workspace_name(&self) -> Option<String> {
|
||||
self.focused_workspace()
|
||||
.map(|w| w.name().clone())
|
||||
.map(|w| w.name.clone())
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
pub fn focused_workspace_layout(&self) -> Option<Layout> {
|
||||
self.focused_workspace().and_then(|workspace| {
|
||||
if *workspace.tile() {
|
||||
Some(workspace.layout().clone())
|
||||
if workspace.tile {
|
||||
Some(workspace.layout.clone())
|
||||
} else {
|
||||
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 hmonitor = self.id();
|
||||
let hmonitor = self.id;
|
||||
let monitor_wp = self.wallpaper.clone();
|
||||
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
|
||||
if i == focused_idx {
|
||||
@@ -202,10 +185,10 @@ impl Monitor {
|
||||
/// Updates the `globals` field of all workspaces
|
||||
pub fn update_workspaces_globals(&mut self, offset: Option<Rect>) {
|
||||
let container_padding = self
|
||||
.container_padding()
|
||||
.container_padding
|
||||
.or(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
|
||||
let workspace_padding = self
|
||||
.workspace_padding()
|
||||
.workspace_padding
|
||||
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
|
||||
let (border_width, border_offset) = {
|
||||
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
|
||||
@@ -217,10 +200,11 @@ impl Monitor {
|
||||
(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 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 = self.window_based_work_area_offset;
|
||||
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit;
|
||||
let floating_layer_behaviour = self.floating_layer_behaviour;
|
||||
|
||||
for workspace in self.workspaces_mut() {
|
||||
workspace.globals = WorkspaceGlobals {
|
||||
@@ -232,6 +216,7 @@ impl Monitor {
|
||||
work_area_offset,
|
||||
window_based_work_area_offset,
|
||||
window_based_work_area_offset_limit,
|
||||
floating_layer_behaviour,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,10 +224,10 @@ impl Monitor {
|
||||
/// Updates the `globals` field of workspace with index `workspace_idx`
|
||||
pub fn update_workspace_globals(&mut self, workspace_idx: usize, offset: Option<Rect>) {
|
||||
let container_padding = self
|
||||
.container_padding()
|
||||
.container_padding
|
||||
.or(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
|
||||
let workspace_padding = self
|
||||
.workspace_padding()
|
||||
.workspace_padding
|
||||
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
|
||||
let (border_width, border_offset) = {
|
||||
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
|
||||
@@ -254,10 +239,11 @@ impl Monitor {
|
||||
(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 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 = self.window_based_work_area_offset;
|
||||
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit;
|
||||
let floating_layer_behaviour = self.floating_layer_behaviour;
|
||||
|
||||
if let Some(workspace) = self.workspaces_mut().get_mut(workspace_idx) {
|
||||
workspace.globals = WorkspaceGlobals {
|
||||
@@ -269,6 +255,7 @@ impl Monitor {
|
||||
work_area_offset,
|
||||
window_based_work_area_offset,
|
||||
window_based_work_area_offset_limit,
|
||||
floating_layer_behaviour,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,14 +264,14 @@ impl Monitor {
|
||||
&mut self,
|
||||
container: Container,
|
||||
workspace_idx: Option<usize>,
|
||||
) -> Result<()> {
|
||||
) -> eyre::Result<()> {
|
||||
let workspace = if let Some(idx) = workspace_idx {
|
||||
self.workspaces_mut()
|
||||
.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 {
|
||||
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);
|
||||
@@ -301,21 +288,21 @@ impl Monitor {
|
||||
container: Container,
|
||||
workspace_idx: Option<usize>,
|
||||
direction: OperationDirection,
|
||||
) -> Result<()> {
|
||||
) -> eyre::Result<()> {
|
||||
let workspace = if let Some(idx) = workspace_idx {
|
||||
self.workspaces_mut()
|
||||
.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 {
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
.ok_or_eyre("there is no workspace")?
|
||||
};
|
||||
|
||||
match direction {
|
||||
OperationDirection::Left => {
|
||||
// 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)
|
||||
match workspace.layout() {
|
||||
match workspace.layout {
|
||||
Layout::Default(layout) => match layout {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
workspace.add_container_to_front(container);
|
||||
@@ -339,7 +326,7 @@ impl Monitor {
|
||||
OperationDirection::Right => {
|
||||
// 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)
|
||||
match workspace.layout() {
|
||||
match workspace.layout {
|
||||
Layout::Default(layout) => {
|
||||
let target_index = layout.leftmost_index(workspace.containers().len());
|
||||
|
||||
@@ -403,12 +390,12 @@ impl Monitor {
|
||||
target_workspace_idx: usize,
|
||||
follow: bool,
|
||||
direction: Option<OperationDirection>,
|
||||
) -> Result<()> {
|
||||
) -> eyre::Result<()> {
|
||||
let workspace = self
|
||||
.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");
|
||||
}
|
||||
|
||||
@@ -431,12 +418,12 @@ impl Monitor {
|
||||
};
|
||||
|
||||
target_workspace.floating_windows_mut().push_back(window);
|
||||
target_workspace.set_layer(WorkspaceLayer::Floating);
|
||||
target_workspace.layer = WorkspaceLayer::Floating;
|
||||
}
|
||||
} else {
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
.ok_or_eyre("there is no container")?;
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
|
||||
@@ -449,7 +436,19 @@ impl Monitor {
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
target_workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
if target_workspace.monocle_container.is_some() {
|
||||
for container in target_workspace.containers_mut() {
|
||||
container.restore();
|
||||
}
|
||||
|
||||
for window in target_workspace.floating_windows_mut() {
|
||||
window.restore();
|
||||
}
|
||||
|
||||
target_workspace.reintegrate_monocle_container()?;
|
||||
}
|
||||
|
||||
target_workspace.layer = WorkspaceLayer::Tiling;
|
||||
|
||||
if let Some(direction) = direction {
|
||||
self.add_container_with_direction(
|
||||
@@ -470,7 +469,7 @@ impl Monitor {
|
||||
}
|
||||
|
||||
#[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");
|
||||
|
||||
{
|
||||
@@ -479,7 +478,7 @@ impl Monitor {
|
||||
if workspaces.get(idx).is_none() {
|
||||
workspaces.resize(idx + 1, Workspace::default());
|
||||
}
|
||||
|
||||
self.last_focused_workspace = Some(self.workspaces.focused_idx());
|
||||
self.workspaces.focus(idx);
|
||||
}
|
||||
|
||||
@@ -489,8 +488,8 @@ impl Monitor {
|
||||
if name.is_some() {
|
||||
self.workspaces_mut()
|
||||
.get_mut(idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
.set_name(name);
|
||||
.ok_or_eyre("there is no workspace")?
|
||||
.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,9 +500,9 @@ impl Monitor {
|
||||
self.workspaces().len()
|
||||
}
|
||||
|
||||
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> Result<()> {
|
||||
let offset = if self.work_area_offset().is_some() {
|
||||
self.work_area_offset()
|
||||
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> eyre::Result<()> {
|
||||
let offset = if self.work_area_offset.is_some() {
|
||||
self.work_area_offset
|
||||
} else {
|
||||
offset
|
||||
};
|
||||
@@ -511,7 +510,7 @@ impl Monitor {
|
||||
let focused_workspace_idx = self.focused_workspace_idx();
|
||||
self.update_workspace_globals(focused_workspace_idx, offset);
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
.ok_or_eyre("there is no workspace")?
|
||||
.update()?;
|
||||
|
||||
Ok(())
|
||||
@@ -610,6 +609,25 @@ mod tests {
|
||||
assert_eq!(m.workspaces().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_nonexistent_workspace() {
|
||||
let mut m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Try to remove a workspace that doesn't exist
|
||||
let removed_workspace = m.remove_workspace_by_idx(1);
|
||||
|
||||
// Should return None since there is no workspace at index 1
|
||||
assert!(removed_workspace.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focus_workspace() {
|
||||
let mut m = Monitor::new(
|
||||
@@ -717,6 +735,46 @@ mod tests {
|
||||
assert_eq!(m.focused_workspace().unwrap().containers().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_container_to_nonexistent_workspace() {
|
||||
let mut m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
{
|
||||
// Create workspace 1 and add 3 containers
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
for _ in 0..3 {
|
||||
let container = Container::default();
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
|
||||
// Should have 3 containers in workspace 1
|
||||
assert_eq!(m.focused_workspace().unwrap().containers().len(), 3);
|
||||
}
|
||||
|
||||
// Should only have 1 workspace
|
||||
assert_eq!(m.workspaces().len(), 1);
|
||||
|
||||
// Try to move a container to a workspace that doesn't exist
|
||||
m.move_container_to_workspace(8, true, None).unwrap();
|
||||
|
||||
// Should have 9 workspaces now
|
||||
assert_eq!(m.workspaces().len(), 9);
|
||||
|
||||
// Should be focused on workspace 8
|
||||
assert_eq!(m.focused_workspace_idx(), 8);
|
||||
|
||||
// Should have 1 container in workspace 8
|
||||
assert_eq!(m.focused_workspace().unwrap().containers().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_workspace_count_workspace_contains_two_workspaces() {
|
||||
let mut m = Monitor::new(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_DISPLAY_ADAPTER;
|
||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_MONITOR;
|
||||
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::System::Power::POWERBROADCAST_SETTING;
|
||||
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_VREDRAW;
|
||||
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_DEVTYP_DEVICEINTERFACE;
|
||||
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::PBT_APMRESUMEAUTOMATIC;
|
||||
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::REGISTER_NOTIFICATION_FLAGS;
|
||||
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_DISPLAYCHANGE;
|
||||
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::WTS_SESSION_LOCK;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
|
||||
use windows::core::PCWSTR;
|
||||
|
||||
use crate::WindowsApi;
|
||||
use crate::monitor_reconciliator;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
|
||||
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -224,14 +224,18 @@ impl Hidden {
|
||||
WM_WTSSESSION_CHANGE => {
|
||||
match wparam.0 as u32 {
|
||||
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::MonitorNotification::SessionLocked,
|
||||
);
|
||||
}
|
||||
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::MonitorNotification::SessionUnlocked,
|
||||
@@ -251,7 +255,8 @@ impl Hidden {
|
||||
// and resolution changes here
|
||||
WM_DISPLAYCHANGE => {
|
||||
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(
|
||||
@@ -265,8 +270,8 @@ impl Hidden {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
if wparam.0 as u32 == SPI_SETWORKAREA.0 {
|
||||
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::MonitorNotification::WorkAreaChanged,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user