mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-12 06:20:25 +01:00
Compare commits
154 Commits
feature/fi
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e52796c24d | ||
|
|
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 | ||
|
|
6db73151f7 | ||
|
|
2d6ff0708f | ||
|
|
13a519fb29 | ||
|
|
9f8e4b9dca | ||
|
|
5a0196ac9d | ||
|
|
46d0e340f9 | ||
|
|
371ef88ecb | ||
|
|
f5b5070436 | ||
|
|
c8320552b0 | ||
|
|
2a5a960c34 | ||
|
|
10ab43a8ae | ||
|
|
0e8ed8aa40 | ||
|
|
fa2ccad5bf | ||
|
|
3c4ccd2504 | ||
|
|
7d821cd3db | ||
|
|
f4bbee0a2e | ||
|
|
2934d011fd | ||
|
|
71762a5961 | ||
|
|
76aeefa9f7 | ||
|
|
4968b0fe37 | ||
|
|
b4b400b236 | ||
|
|
2ee0bbc0c7 | ||
|
|
d38d3c956d | ||
|
|
052eb1c763 | ||
|
|
58730b81b4 | ||
|
|
274ae43e8f | ||
|
|
2a30f09bbd | ||
|
|
8fd18048a4 | ||
|
|
5809735024 | ||
|
|
96fdbbd1fb | ||
|
|
de131e9ca5 | ||
|
|
07dba03255 | ||
|
|
acd53dec1b | ||
|
|
a98968d179 | ||
|
|
8a32219867 | ||
|
|
ce4b75cc3c | ||
|
|
e4226ce623 | ||
|
|
4bfd7febb4 | ||
|
|
5cc688dc6b | ||
|
|
d897890032 | ||
|
|
e702d93a8a | ||
|
|
a8c687d3d5 | ||
|
|
30fbc1ae73 | ||
|
|
cb60e91842 | ||
|
|
64d29d606a | ||
|
|
072a62c314 | ||
|
|
a95e6e9644 | ||
|
|
6ba19d3ea2 | ||
|
|
edf1943157 | ||
|
|
d0c847e5bc | ||
|
|
992bc2abfe | ||
|
|
fa07f2d2f8 | ||
|
|
cc4e204191 | ||
|
|
24791f0ce5 | ||
|
|
6b95bf95f9 | ||
|
|
c0e1e9366d | ||
|
|
532436fe1a | ||
|
|
532949409c | ||
|
|
ec4a5e6491 | ||
|
|
f6e99eaac1 | ||
|
|
a6cf801a6b | ||
|
|
83d11c6f0f | ||
|
|
1ba1c57ba0 | ||
|
|
9f16894a09 | ||
|
|
df9ae931cc | ||
|
|
be2af9fdcb | ||
|
|
c083484ef0 | ||
|
|
1804b21c4a | ||
|
|
b6bd191cf5 | ||
|
|
5919f88b38 | ||
|
|
ff2aa5e51a | ||
|
|
42c12d5ec3 | ||
|
|
f0ce8e8572 | ||
|
|
bdea4821c3 | ||
|
|
229aeb7ddc | ||
|
|
17cbdc8663 | ||
|
|
9f3d806f79 | ||
|
|
fe9a1416e7 | ||
|
|
3618beb366 | ||
|
|
a4de2ee841 | ||
|
|
60e1834b43 | ||
|
|
54323c4c6a | ||
|
|
6516c808ee | ||
|
|
894b6f3d96 | ||
|
|
7ccdff4986 | ||
|
|
c48e1db0ff | ||
|
|
ea9752d5e1 | ||
|
|
acf780767c | ||
|
|
8e588d0284 | ||
|
|
91ff9b8852 | ||
|
|
81a7951312 | ||
|
|
555308db5f |
3
.github/workflows/windows.yaml
vendored
3
.github/workflows/windows.yaml
vendored
@@ -55,8 +55,7 @@ jobs:
|
||||
key: ${{ matrix.platform.target }}
|
||||
- run: cargo +nightly fmt --check
|
||||
- run: cargo clippy
|
||||
# sockets don't work properly on the windows runner
|
||||
- run: cargo test -- --skip window_manager --skip process_command
|
||||
- run: cargo test
|
||||
- uses: houseabsolute/actions-rust-cross@v1
|
||||
with:
|
||||
command: "build"
|
||||
|
||||
40
CODE_OF_CONDUCT.md
Normal file
40
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# The Komorebi Code of Conduct
|
||||
|
||||
This document is based on the [Rust Code of
|
||||
Conduct](https://www.rust-lang.org/policies/code-of-conduct)
|
||||
|
||||
## Conduct
|
||||
|
||||
- We are committed to providing a friendly, safe and welcoming environment for
|
||||
all, regardless of level of experience, gender identity and expression, sexual
|
||||
orientation, disability, personal appearance, body size, race, ethnicity, age,
|
||||
religion, nationality, or other similar characteristic.
|
||||
|
||||
- Please avoid using overtly sexual aliases or other nicknames that might
|
||||
detract from a friendly, safe and welcoming environment for all.
|
||||
|
||||
- Please be kind and courteous. There’s no need to be mean or rude.
|
||||
|
||||
- Respect that people have differences of opinion and that every design or
|
||||
implementation choice carries a trade-off and numerous costs. There is seldom a
|
||||
right answer.
|
||||
|
||||
- Please keep unstructured critique to a minimum. If you have solid ideas you
|
||||
want to experiment with, make a fork and see how it works.
|
||||
|
||||
- We will exclude you from interaction if you insult, demean or harass anyone.
|
||||
That is not welcome behavior. We interpret the term “harassment” as including
|
||||
the definition in the [Citizen Code of
|
||||
Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md);
|
||||
if you have any lack of clarity about what might be included in that concept,
|
||||
please read their definition. In particular, we don’t tolerate behavior that
|
||||
excludes people in socially marginalized groups.
|
||||
|
||||
- Private harassment is also unacceptable. No matter who you are, if you feel
|
||||
you have been or are being harassed or made uncomfortable by a community member,
|
||||
please contact me immediately. Whether you’re a regular contributor or a
|
||||
newcomer, we care about making this community a safe place for you and we’ve got
|
||||
your back.
|
||||
|
||||
- Likewise any spamming, trolling, flaming, baiting or other attention-stealing
|
||||
behavior is not welcome.
|
||||
2215
Cargo.lock
generated
2215
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
19
Cargo.toml
@@ -8,11 +8,13 @@ members = [
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes"
|
||||
"komorebi-themes",
|
||||
"komorebi-shortcuts"
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = { version = "4", features = ["derive", "wrap_help"] }
|
||||
chrono-tz = "0.10"
|
||||
chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
@@ -31,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.34"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "55cebdebfbd68dbd14945a1ba90f6b05b7be2893" }
|
||||
windows-numerics = { version = "0.1" }
|
||||
windows-implement = { version = "0.59" }
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "a28c6559a9de2f92c142a714947a9b081776caca" }
|
||||
windows-numerics = { version = "0.2" }
|
||||
windows-implement = { version = "0.60" }
|
||||
windows-interface = { version = "0.59" }
|
||||
windows-core = { version = "0.60" }
|
||||
windows-core = { version = "0.61" }
|
||||
shadow-rs = "1"
|
||||
which = "7"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.60"
|
||||
version = "0.61"
|
||||
features = [
|
||||
"Foundation_Numerics",
|
||||
"Win32_Devices",
|
||||
@@ -71,4 +74,4 @@ features = [
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control"
|
||||
]
|
||||
]
|
||||
|
||||
24
LICENSE.md
24
LICENSE.md
@@ -1,6 +1,6 @@
|
||||
# Komorebi License
|
||||
|
||||
Version 1.0.0
|
||||
Version 2.0.0
|
||||
|
||||
## Acceptance
|
||||
|
||||
@@ -13,9 +13,20 @@ your licenses.
|
||||
The licensor grants you a copyright license for the software
|
||||
to do everything you might do with the software that would
|
||||
otherwise infringe the licensor's copyright in it for any
|
||||
permitted purpose. However, you may only make changes according
|
||||
permitted purpose. However, you may only distribute the source
|
||||
code of the software according to the [Distribution License](
|
||||
#distribution-license), you may only make changes according
|
||||
to the [Changes License](#changes-license), and you may not
|
||||
distribute the software or new works based on the software.
|
||||
otherwise distribute the software or new works based on the
|
||||
software.
|
||||
|
||||
## Distribution License
|
||||
|
||||
The licensor grants you an additional copyright license to
|
||||
distribute copies of the source code of the software. Your
|
||||
license to distribute covers distributing the source code of
|
||||
the software with changes permitted by the [Changes License](
|
||||
#changes-license).
|
||||
|
||||
## Changes License
|
||||
|
||||
@@ -45,7 +56,7 @@ law. These terms do not limit them.
|
||||
|
||||
These terms do not allow you to sublicense or transfer any of
|
||||
your licenses to anyone else, or prevent the licensor from
|
||||
granting licenses to anyone else. These terms do not imply
|
||||
granting licenses to anyone else. These terms do not imply
|
||||
any other licenses.
|
||||
|
||||
## Patent Defense
|
||||
@@ -63,7 +74,7 @@ violated any of these terms, or done anything with the software
|
||||
not covered by your licenses, your licenses can nonetheless
|
||||
continue if you come into full compliance with these terms,
|
||||
and take practical steps to correct past violations, within
|
||||
32 days of receiving notice. Otherwise, all your licenses
|
||||
32 days of receiving notice. Otherwise, all your licenses
|
||||
end immediately.
|
||||
|
||||
## No Liability
|
||||
@@ -88,11 +99,10 @@ organizations that have control over, are under the control of,
|
||||
or are under common control with that organization. **Control**
|
||||
means ownership of substantially all the assets of an entity,
|
||||
or the power to direct its management and policies by vote,
|
||||
contract, or otherwise. Control can be direct or indirect.
|
||||
contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
**Your licenses** are all the licenses granted to you for the
|
||||
software under these terms.
|
||||
|
||||
**Use** means anything you do with the software requiring one
|
||||
of your licenses.
|
||||
|
||||
|
||||
15
README.md
15
README.md
@@ -71,7 +71,10 @@ showcases the many awesome projects that exist in the _komorebi_ ecosystem.
|
||||
|
||||
## Licensing for Personal Use
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 1.0.0
|
||||
`komorebi` is [educational source
|
||||
software](https://lgug2z.com/articles/educational-source-software/).
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 2.0.0
|
||||
license](https://github.com/LGUG2Z/komorebi-license), which is a fork of the
|
||||
[PolyForm Strict 1.0.0
|
||||
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
|
||||
@@ -82,7 +85,7 @@ hard-forks) based on the software.
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended either
|
||||
for personal use or for integration back upstream via pull requests.
|
||||
|
||||
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does
|
||||
The [Komorebi 2.0.0 License](https://github.com/LGUG2Z/komorebi-license) does
|
||||
not permit any kind of commercial use (i.e. using `komorebi` at work).
|
||||
|
||||
## Sponsorship for Personal Use
|
||||
@@ -99,7 +102,8 @@ me on GitHub.
|
||||
[GitHub Sponsors is enabled for this
|
||||
project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on
|
||||
the Discord server, get shout outs at the end of _komorebi_-related videos on
|
||||
YouTube, and gain the ability to submit feature requests on the issue tracker.
|
||||
YouTube, gain the ability to submit feature requests on the issue tracker, and
|
||||
receive releases of komorebi with "easter eggs" on physical media.
|
||||
|
||||
If you would like to tip or sponsor the project but are unable to use GitHub
|
||||
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
|
||||
@@ -142,7 +146,8 @@ video will answer the majority of your questions.
|
||||
|
||||
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
|
||||
unfocused window transparency and animations enabled, using a custom status bar integrated using
|
||||
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
|
||||
_komorebi_'
|
||||
s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
|
||||
|
||||
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
|
||||
|
||||
@@ -389,7 +394,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.34"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.37"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
|
||||
37
deny.toml
37
deny.toml
@@ -13,7 +13,8 @@ feature-depth = 1
|
||||
[advisories]
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" }
|
||||
{ id = "RUSTSEC-2024-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" }
|
||||
]
|
||||
|
||||
[licenses]
|
||||
@@ -33,43 +34,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]]
|
||||
@@ -93,4 +109,7 @@ allow-git = [
|
||||
"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",
|
||||
]
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
[
|
||||
"0BSD",
|
||||
[
|
||||
"adler 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"adler2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"win32-display-data 0.1.0 git+https://github.com/LGUG2Z/win32-display-data?rev=55cebdebfbd68dbd14945a1ba90f6b05b7be2893"
|
||||
"win32-display-data 0.1.0 git+https://github.com/LGUG2Z/win32-display-data?rev=a28c6559a9de2f92c142a714947a9b081776caca"
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -16,74 +17,85 @@
|
||||
"accesskit_consumer 0.26.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"accesskit_windows 0.24.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"accesskit_winit 0.23.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"adler 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"adler2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ahash 0.8.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ahash 0.8.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"allocator-api2 0.2.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstream 0.6.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle 1.0.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-parse 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-query 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-wincon 3.0.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anyhow 1.0.97 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arboard 3.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anyhow 1.0.98 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"approx 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arboard 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arrayvec 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"atomic-waker 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"autocfg 1.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"backtrace 0.3.71 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"backtrace 0.3.75 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"backtrace-ext 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"base16_color_scheme 0.3.2 git+https://github.com/LGUG2Z/base16_color_scheme",
|
||||
"base64 0.22.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"beef 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bit_field 0.10.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitflags 1.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitflags 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitflags 2.9.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitstream-io 2.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck 1.22.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck_derive 1.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cc 1.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck 1.23.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck_derive 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cc 1.2.23 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cfg-if 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cfg-if 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chrono 0.4.40 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap 4.5.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_builder 4.5.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_derive 4.5.28 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chrono 0.4.41 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chrono-tz 0.10.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chrono-tz-build 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap 4.5.38 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_builder 4.5.38 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_derive 4.5.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_lex 0.7.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-eyre 0.6.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-spantrace 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-eyre 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-spantrace 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"colorchoice 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crc32fast 1.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-channel 0.5.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-channel 0.5.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ctrlc 3.4.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ctrlc 3.4.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cursor-icon 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"deranged 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"deflate 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"deranged 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs 3.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs 4.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs 6.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs-sys 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs-sys 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"displaydoc 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"document-features 0.2.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dpi 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dpi 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dyn-clone 1.0.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ecolor 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eframe 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ecolor 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eframe 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui-phosphor 0.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui-winit 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_extras 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_glow 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"either 1.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"emath 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui-winit 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_extras 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_glow 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"either 1.15.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"emath 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map 2.7.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map-derive 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"env_home 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint_default_fonts 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint_default_fonts 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fastrand 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fdeflate 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"filetime 0.2.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"flate2 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"flate2 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"form_urlencoded 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -95,53 +107,67 @@
|
||||
"futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.1.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"gif 0.11.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"gif 0.13.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"git2 0.20.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"git2 0.20.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"gl_generator 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glob 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glutin 0.32.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glutin 0.32.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glutin_egl_sys 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glutin_wgl_sys 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"half 2.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"half 2.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.14.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.15.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hex 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hex_color 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hotwatch 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http 1.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"iana-time-zone 0.1.61 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"iana-time-zone 0.1.63 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ident_case 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna_adapter 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image 0.25.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna_adapter 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image 0.25.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image-webp 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"imgref 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"immutable-chunkmap 2.0.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indenter 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 2.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"is_debug 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"is_terminal_polyfill 1.70.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itertools 0.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itertools 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itoa 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jobserver 0.1.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jobserver 0.1.33 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jpeg-decoder 0.1.22 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jpeg-decoder 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"khronos_api 3.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"lazy_static 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libc 0.2.170 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libgit2-sys 0.18.0+1.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libz-sys 1.1.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libc 0.2.172 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libgit2-sys 0.18.1+1.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libz-sys 1.1.22 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"linked-hash-map 0.5.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"litrs 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"lock_api 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"log 0.4.26 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miette 7.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miette-derive 7.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"log 0.4.27 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"logos 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"logos-codegen 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"logos-derive 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miette 7.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miette-derive 7.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mime 0.3.17 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"minimal-lexical 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miow 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"native-tls 0.2.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"net2 0.2.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -154,10 +180,13 @@
|
||||
"num-derive 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-integer 0.1.46 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-iter 0.1.45 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-rational 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-rational 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-traits 0.2.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"once_cell 1.20.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"once_cell 1.21.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"owned_ttf_parser 0.25.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"palette 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"palette_derive 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot_core 0.9.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -165,20 +194,26 @@
|
||||
"pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"png 0.16.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"png 0.17.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ppv-lite86 0.2.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ppv-lite86 0.2.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro-error-attr2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro-error2 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro2 1.0.94 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro2 1.0.95 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"profiling 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"profiling-procmacros 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"psm 0.1.26 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"qoi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quick-error 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quote 1.0.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quote 1.0.40 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.7.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_pcg 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon 1.10.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon-core 1.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -186,46 +221,58 @@
|
||||
"regex-automata 0.4.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-syntax 0.6.29 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-syntax 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"reqwest 0.12.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"reqwest 0.12.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"roxmltree 0.20.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustc-demangle 0.1.24 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pki-types 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pki-types 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustversion 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ryu 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"scopeguard 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde 1.0.218 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive 1.0.218 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde 1.0.219 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive 1.0.219 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive_internals 0.29.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_json 1.0.140 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_json_lenient 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_urlencoded 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_variant 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_with 3.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_with_macros 3.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_yaml 0.8.26 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_yaml 0.9.34+deprecated registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shadow-rs 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shadow-rs 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shell-words 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shellexpand 2.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"smallvec 1.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"siphasher 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"siphasher 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"smallvec 1.15.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"smol_str 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"socket2 0.5.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"socket2 0.5.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"stable_deref_trait 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"stacker 0.1.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"static_assertions 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"supports-color 3.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"supports-hyperlinks 3.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"supports-unicode 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"syn 2.0.99 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"syn 1.0.109 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"syn 2.0.101 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"sync_wrapper 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tempfile 3.17.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"terminal_size 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tempfile 3.20.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"terminal_size 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror-impl 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror-impl 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thread_local 1.1.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time 0.3.37 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time-core 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time 0.3.41 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time-core 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"toml 0.5.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ttf-parser 0.25.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"typenum 1.18.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tz-rs 0.7.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tzdb 0.7.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tzdb_data 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicase 2.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-linebreak 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -235,54 +282,64 @@
|
||||
"unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"uom 0.36.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"url 2.5.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf16_iter 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"web-time 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"webbrowser 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"webbrowser 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"weezl 0.1.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.61.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-collections 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-collections 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.60.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.61.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-future 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-future 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-link 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.59.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-link 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-numerics 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-registry 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-numerics 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-registry 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-strings 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-strings 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-strings 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.48.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-threading 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_aarch64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_aarch64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_aarch64_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_i686_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_i686_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_i686_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winit 0.30.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"wmi 0.15.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"write16 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winit 0.30.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"wmi 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yaml-rust 0.4.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy 0.8.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zeroize 1.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-jpeg 0.4.14 registry+https://github.com/rust-lang/crates.io-index"
|
||||
@@ -301,8 +358,7 @@
|
||||
"av1-grain 0.2.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rav1e 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"v_frame 0.3.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.7.35 registry+https://github.com/rust-lang/crates.io-index"
|
||||
"zerocopy 0.8.25 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -316,14 +372,14 @@
|
||||
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"exr 1.73.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"lebe 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ravif 0.11.11 registry+https://github.com/rust-lang/crates.io-index"
|
||||
"ravif 0.11.12 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"BSL-1.0",
|
||||
[
|
||||
"clipboard-win 5.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"error-code 3.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"error-code 3.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ryu 1.0.20 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
@@ -341,9 +397,9 @@
|
||||
"ISC",
|
||||
[
|
||||
"is_ci 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libloading 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libloading 0.8.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"starship-battery 0.10.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
"starship-battery 0.10.1 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -352,84 +408,103 @@
|
||||
"accesskit 0.17.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"accesskit_consumer 0.26.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"accesskit_windows 0.24.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"adler 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"adler2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ahash 0.8.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ahash 0.8.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"aho-corasick 1.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"aligned-vec 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"allocator-api2 0.2.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstream 0.6.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle 1.0.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-parse 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-query 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anstyle-wincon 3.0.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anyhow 1.0.97 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arboard 3.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"anyhow 1.0.98 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arboard 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arg_enum_proc_macro 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"arrayvec 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"atomic-waker 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"autocfg 1.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"backtrace 0.3.71 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"backtrace 0.3.75 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"backtrace-ext 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"base16_color_scheme 0.3.2 git+https://github.com/LGUG2Z/base16_color_scheme",
|
||||
"base64 0.22.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"beef 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bit_field 0.10.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitflags 1.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitflags 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitflags 2.9.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bitstream-io 2.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"brotli 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"brotli-decompressor 2.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"built 0.7.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck 1.22.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck_derive 1.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck 1.23.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck_derive 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"byteorder 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"byteorder-lite 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytes 1.10.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytes 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"calm_io 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"calmio_filters 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"catppuccin-egui 5.3.1 git+https://github.com/LGUG2Z/catppuccin-egui?rev=bdaff30959512c4f7ee7304117076a48633d777f",
|
||||
"cc 1.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cc 1.2.23 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cfg-if 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cfg-if 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cfg_aliases 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chrono 0.4.40 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap 4.5.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_builder 4.5.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_derive 4.5.28 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chrono 0.4.41 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chrono-tz 0.10.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chrono-tz-build 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"chumsky 0.9.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap 4.5.38 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_builder 4.5.38 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_derive 4.5.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"clap_lex 0.7.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-eyre 0.6.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-spantrace 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-eyre 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-spantrace 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color-thief 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"color_quant 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"colorchoice 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crc32fast 1.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-channel 0.5.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-channel 0.5.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ctrlc 3.4.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ctrlc 3.4.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cursor-icon 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"deranged 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"darling 0.20.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"darling_core 0.20.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"darling_macro 0.20.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"deflate 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"deranged 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs 3.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs 4.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs 6.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs-sys 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dirs-sys 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"displaydoc 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"document-features 0.2.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dpi 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"dyn-clone 1.0.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ecolor 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eframe 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ecolor 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eframe 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui-phosphor 0.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui-winit 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_extras 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_glow 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"either 1.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"emath 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui-winit 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_extras 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"egui_glow 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"either 1.15.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"emath 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map 2.7.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"enum-map-derive 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"env_home 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint_default_fonts 0.31.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"epaint_default_fonts 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fastrand 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"fdeflate 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"filetime 0.2.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"flate2 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"flate2 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"flavours 0.7.2 git+https://github.com/LGUG2Z/flavours",
|
||||
"fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"font-loader 0.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"form_urlencoded 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -443,64 +518,80 @@
|
||||
"futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.1.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getrandom 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"getset 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"gif 0.11.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"gif 0.13.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"git2 0.20.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"git2 0.20.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glob 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glutin-winit 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"h2 0.4.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"half 2.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"h2 0.4.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"half 2.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.14.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hashbrown 0.15.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hex 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hex_color 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hotwatch 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http 1.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http-body 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http-body-util 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"http-body-util 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper 1.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper-util 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"iana-time-zone 0.1.61 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"hyper-util 0.1.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"iana-time-zone 0.1.63 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ident_case 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna_adapter 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image 0.25.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"idna_adapter 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image 0.23.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image 0.25.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"image-webp 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"immutable-chunkmap 2.0.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indenter 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 2.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"indexmap 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"is_debug 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"is_terminal_polyfill 1.70.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itertools 0.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itertools 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"itoa 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jobserver 0.1.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jobserver 0.1.33 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jpeg-decoder 0.1.22 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"jpeg-decoder 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"lazy_static 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libc 0.2.170 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libgit2-sys 0.18.0+1.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libz-sys 1.1.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libc 0.2.172 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libgit2-sys 0.18.1+1.9.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"libz-sys 1.1.22 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"linked-hash-map 0.5.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"litrs 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"lock_api 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"log 0.4.26 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"log 0.4.27 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"logos 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"logos-codegen 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"logos-derive 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"loop9 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"matchers 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"maybe-rayon 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"memchr 2.7.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"memoffset 0.9.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mime 0.3.17 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mime_guess2 2.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mime_guess2 2.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"minimal-lexical 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"mio 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miow 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"nanoid 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"native-tls 0.2.14 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"net2 0.2.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"netdev 0.32.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"netdev 0.34.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"new_debug_unreachable 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"nohash-hasher 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"nom 7.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -514,36 +605,53 @@
|
||||
"num-derive 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-integer 0.1.46 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-iter 0.1.45 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-rational 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-rational 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"num-traits 0.2.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"once_cell 1.20.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"os_info 3.10.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"once_cell 1.21.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"os_info 3.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"overload 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"owo-colors 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"owo-colors 4.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"owo-colors 4.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"palette 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"palette_derive 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parking_lot_core 0.9.10 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"parse-zoneinfo 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"percent-encoding 2.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_codegen 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_codegen 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_generator 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_generator 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_shared 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"phf_shared 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"png 0.16.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"png 0.17.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"powershell_script 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ppv-lite86 0.2.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ppv-lite86 0.2.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro-error-attr2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro-error2 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro2 1.0.94 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"proc-macro2 1.0.95 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"profiling 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"profiling-procmacros 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"psm 0.1.26 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"qoi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quick-error 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quote 1.0.39 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"quote 1.0.40 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.7.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_chacha 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_core 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"random_word 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rand_pcg 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"random_word 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon 1.10.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rayon-core 1.12.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -552,56 +660,70 @@
|
||||
"regex-automata 0.4.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-syntax 0.6.29 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"regex-syntax 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"reqwest 0.12.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"reqwest 0.12.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rgb 0.8.50 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"roxmltree 0.20.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustc-demangle 0.1.24 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pki-types 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustls-pki-types 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"rustversion 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"same-file 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"schannel 0.1.27 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"schemars 0.8.22 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"schemars_derive 0.8.22 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"scoped_threadpool 0.1.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"scopeguard 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde 1.0.218 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive 1.0.218 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde 1.0.219 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive 1.0.219 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_derive_internals 0.29.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_json 1.0.140 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_json_lenient 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_urlencoded 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_variant 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_with 3.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_with_macros 3.12.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_yaml 0.8.26 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"serde_yaml 0.9.34+deprecated registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shadow-rs 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shadow-rs 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"sharded-slab 0.1.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shell-words 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shellexpand 2.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"simd-adler32 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"simd_helpers 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"siphasher 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"siphasher 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"slab 0.4.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"smallvec 1.14.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"smallvec 1.15.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"smol_str 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"socket2 0.5.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"socket2 0.5.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"stable_deref_trait 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"stacker 0.1.21 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"static_assertions 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"strsim 0.11.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"strum 0.27.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"strum_macros 0.27.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"syn 2.0.99 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"synstructure 0.13.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"syn 1.0.109 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"syn 2.0.101 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"synstructure 0.13.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"sysinfo 0.33.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tempfile 3.17.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"terminal_size 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"sysinfo 0.34.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tempfile 3.20.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"terminal_size 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"textwrap 0.16.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror-impl 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thiserror-impl 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"thread_local 1.1.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tiff 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tiff 0.9.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time 0.3.37 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time-core 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio 1.43.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time 0.3.41 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"time-core 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio 1.45.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio-native-tls 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio-util 0.7.13 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tokio-util 0.7.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"toml 0.5.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tower 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tower-layer 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tower-service 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -616,6 +738,7 @@
|
||||
"ttf-parser 0.25.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"typenum 1.18.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tz-rs 0.7.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tzdb_data 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"uds_windows 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicase 2.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -626,7 +749,6 @@
|
||||
"unsafe-libyaml 0.2.11 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"uom 0.36.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"url 2.5.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf16_iter 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
|
||||
@@ -634,54 +756,66 @@
|
||||
"walkdir 2.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"want 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"web-time 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"webbrowser 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"webbrowser 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"weezl 0.1.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"which 7.0.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"which 7.0.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winapi-util 0.1.9 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows 0.61.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-collections 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-collections 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.60.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-core 0.61.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-future 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-future 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-icons 0.1.0 git+https://github.com/LGUG2Z/windows-icons?rev=0c9d7ee1b807347c507d3a9862dd007b4d3f4354",
|
||||
"windows-icons 0.1.0 git+https://github.com/LGUG2Z/windows-icons?rev=d67cc9920aa9b4883393e411fb4fa2ddd4c498b5",
|
||||
"windows-implement 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-implement 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-link 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-interface 0.59.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-link 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-numerics 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-registry 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-numerics 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-registry 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-result 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-strings 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-strings 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-strings 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.48.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-targets 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows-threading 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_aarch64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_aarch64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_aarch64_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_i686_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_i686_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_i686_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"windows_x86_64_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winput 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winreg 0.55.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"winsafe 0.0.19 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"wmi 0.15.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"write16 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"xml-rs 0.8.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy-derive 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"wmi 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"xml-rs 0.8.26 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yaml-rust 0.4.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerocopy 0.8.25 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zeroize 1.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-jpeg 0.4.14 registry+https://github.com/rust-lang/crates.io-index"
|
||||
@@ -691,50 +825,50 @@
|
||||
"MIT-0",
|
||||
[
|
||||
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tzdb_data 0.2.1 registry+https://github.com/rust-lang/crates.io-index"
|
||||
"tzdb_data 0.2.2 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"MPL-2.0",
|
||||
[
|
||||
"option-ext 0.2.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
"option-ext 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"ramhorns 1.0.1 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"OFL-1.1",
|
||||
[
|
||||
"epaint_default_fonts 0.31.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
"epaint_default_fonts 0.31.1 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"Ubuntu-font-1.0",
|
||||
[
|
||||
"epaint_default_fonts 0.31.0 registry+https://github.com/rust-lang/crates.io-index"
|
||||
"epaint_default_fonts 0.31.1 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
"Unicode-3.0",
|
||||
[
|
||||
"icu_collections 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_locid 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_locid_transform 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_locid_transform_data 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_normalizer 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_normalizer_data 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_properties 1.5.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_properties_data 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_provider 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_provider_macros 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"litemap 0.7.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tinystr 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_collections 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_locale_core 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_normalizer 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_normalizer_data 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_properties 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_properties_data 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"icu_provider 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"litemap 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"potential_utf 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"tinystr 0.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"writeable 0.5.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yoke 0.7.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yoke-derive 0.7.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"writeable 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yoke 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"yoke-derive 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerofrom 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerofrom-derive 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerovec 0.10.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerovec-derive 0.10.3 registry+https://github.com/rust-lang/crates.io-index"
|
||||
"zerotrie 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerovec 0.11.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zerovec-derive 0.11.1 registry+https://github.com/rust-lang/crates.io-index"
|
||||
]
|
||||
],
|
||||
[
|
||||
@@ -753,14 +887,16 @@
|
||||
[
|
||||
"Zlib",
|
||||
[
|
||||
"bytemuck 1.22.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck_derive 1.8.1 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"adler32 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck 1.23.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"bytemuck_derive 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"const_format 0.2.34 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"const_format_proc_macros 0.2.34 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"cursor-icon 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"foldhash 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"foldhash 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"miniz_oxide 0.8.8 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
|
||||
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
|
||||
|
||||
@@ -10,9 +10,8 @@ Options:
|
||||
Desired ease function for animation
|
||||
|
||||
[default: linear]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart,
|
||||
ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ,
|
||||
ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint,
|
||||
ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
|
||||
-a, --animation-type <ANIMATION_TYPE>
|
||||
Animation type to apply the style to. If not specified, sets global style
|
||||
|
||||
@@ -18,7 +18,7 @@ Arguments:
|
||||
Options:
|
||||
-w, --window-kind <WINDOW_KIND>
|
||||
[default: single]
|
||||
[possible values: single, stack, monocle, unfocused, floating]
|
||||
[possible values: single, stack, monocle, unfocused, unfocused-locked, floating]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
12
docs/cli/clear-session-float-rules.md
Normal file
12
docs/cli/clear-session-float-rules.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# clear-session-float-rules
|
||||
|
||||
```
|
||||
Clear all session float rules
|
||||
|
||||
Usage: komorebic.exe clear-session-float-rules
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/data-directory.md
Normal file
12
docs/cli/data-directory.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# data-directory
|
||||
|
||||
```
|
||||
Show the path to komorebi's data directory in %LOCALAPPDATA%
|
||||
|
||||
Usage: komorebic.exe data-directory
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/move-to-last-workspace.md
Normal file
12
docs/cli/move-to-last-workspace.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# move-to-last-workspace
|
||||
|
||||
```
|
||||
Move the focused window to the last focused monitor workspace
|
||||
|
||||
Usage: komorebic.exe move-to-last-workspace
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe query <STATE_QUERY>
|
||||
|
||||
Arguments:
|
||||
<STATE_QUERY>
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name]
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, focused-container-kind, version]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
12
docs/cli/send-to-last-workspace.md
Normal file
12
docs/cli/send-to-last-workspace.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# send-to-last-workspace
|
||||
|
||||
```
|
||||
Send the focused window to the last focused monitor workspace
|
||||
|
||||
Usage: komorebic.exe send-to-last-workspace
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/session-float-rule.md
Normal file
12
docs/cli/session-float-rule.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# session-float-rule
|
||||
|
||||
```
|
||||
Add a rule to float the foreground window for the rest of this session
|
||||
|
||||
Usage: komorebic.exe session-float-rule
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/session-float-rules.md
Normal file
12
docs/cli/session-float-rules.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# session-float-rules
|
||||
|
||||
```
|
||||
Show all session float rules
|
||||
|
||||
Usage: komorebic.exe session-float-rules
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/toggle-lock.md
Normal file
12
docs/cli/toggle-lock.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-lock
|
||||
|
||||
```
|
||||
Toggle a lock for the focused container, ensuring it will not be displaced by any new windows
|
||||
|
||||
Usage: komorebic.exe toggle-lock
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
```
|
||||
@@ -1,8 +1,7 @@
|
||||
# toggle-workspace-float-override
|
||||
|
||||
```
|
||||
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace
|
||||
previously it takes the opposite of the global value
|
||||
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace previously it takes the opposite of the global value
|
||||
|
||||
Usage: komorebic.exe toggle-workspace-float-override
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# toggle-workspace-window-container-behaviour
|
||||
|
||||
```
|
||||
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the
|
||||
global value
|
||||
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the global value
|
||||
|
||||
Usage: komorebic.exe toggle-workspace-window-container-behaviour
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ defined in the `komorebi.json` configuration file.
|
||||
```json
|
||||
{
|
||||
"animation": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"duration": 250,
|
||||
"fps": 60,
|
||||
"style": "EaseOutSine"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
419
docs/common-workflows/multi-monitor-setup.md
Normal file
419
docs/common-workflows/multi-monitor-setup.md
Normal file
@@ -0,0 +1,419 @@
|
||||
# Multi-Monitor Setup
|
||||
|
||||
You can set up komorebi to work with multiple monitors. To do so, first you start by setting up multiple monitor
|
||||
configurations on your `komorebi.json` config file.
|
||||
|
||||
If you've used the [`komorebic quickstart`](../cli/quickstart.md) command you'll already have a `komorebi.json` config
|
||||
file with one monitor config setup. Open this file and look for the `"monitors":` line, you should find something like
|
||||
this:
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "I",
|
||||
"layout": "BSP"
|
||||
},
|
||||
{
|
||||
"name": "II",
|
||||
"layout": "VerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "III",
|
||||
"layout": "HorizontalStack"
|
||||
},
|
||||
{
|
||||
"name": "IV",
|
||||
"layout": "UltrawideVerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "V",
|
||||
"layout": "Rows"
|
||||
},
|
||||
{
|
||||
"name": "VI",
|
||||
"layout": "Grid"
|
||||
},
|
||||
{
|
||||
"name": "VII",
|
||||
"layout": "RightMainVerticalStack"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For this example we will remove some workspaces to simplify the config so it is easier to look at, but feel free to
|
||||
set up as many workspaces per monitor as you'd like. Here is the same configuration with only 3 workspaces.
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "I",
|
||||
"layout": "BSP"
|
||||
},
|
||||
{
|
||||
"name": "II",
|
||||
"layout": "VerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "III",
|
||||
"layout": "HorizontalStack"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Let's add another monitor:
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
// monitor 1, index 0
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "I",
|
||||
"layout": "BSP"
|
||||
},
|
||||
{
|
||||
"name": "II",
|
||||
"layout": "VerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "III",
|
||||
"layout": "HorizontalStack"
|
||||
}
|
||||
]
|
||||
},
|
||||
// monitor 2, index 1
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "1",
|
||||
"layout": "BSP"
|
||||
},
|
||||
{
|
||||
"name": "2",
|
||||
"layout": "VerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "3",
|
||||
"layout": "HorizontalStack"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Now have two monitor configurations. We have the first monitor configuration, which is index 0 (*usually
|
||||
on programming languages the first item of a list starts with index 0*), this configuration has 3 workspaces with names
|
||||
"I", "II" and "III". Then the 2nd monitor configuration, which is index 1, also has 3 workspaces with names "1", "2",
|
||||
and "3" (you should always give unique names to your workspaces).
|
||||
|
||||
Now if you start komorebi with two monitors connected, the main monitor will use the configuration with index 0 and the
|
||||
secondary monitor will use the configuration with index 1.
|
||||
|
||||
---
|
||||
|
||||
Let's say you have more monitors, or you want to make sure that a certain configuration is always applied to a certain
|
||||
monitor. For this you will want to use the `display_index_preferences`.
|
||||
|
||||
Open up a terminal and type the following command: [ `komorebic monitor-info`](../cli/monitor-information.md). This
|
||||
command will give you the information about your connected monitors, you want to look up the `serial_number_id`. You
|
||||
should get something like this:
|
||||
|
||||
```
|
||||
❯ komorebic monitor-info
|
||||
[
|
||||
{
|
||||
"id": 6620935,
|
||||
"name": "DISPLAY1",
|
||||
"device": "BOE0A1C",
|
||||
"device_id": "BOE0A1C-5&a2bea0b&0&UID512",
|
||||
"serial_number_id": "0",
|
||||
"size": {
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"right": 1920,
|
||||
"bottom": 1080
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 181932057,
|
||||
"name": "DISPLAY2",
|
||||
"device": "VSC8C31",
|
||||
"device_id": "VSC8C31-5&18560b1f&0&UID4356",
|
||||
"serial_number_id": "UEP174021562",
|
||||
"size": {
|
||||
"left": 0,
|
||||
"top": -1080,
|
||||
"right": 1920,
|
||||
"bottom": 1080
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In this case the setup is a laptop with a secondary monitor connected. You'll need to figure out which monitor is which,
|
||||
usually the display name's number should be similar to the numbers you can find on
|
||||
`Windows Settings -> System -> Display`.
|
||||
|
||||
If you have trouble with this step you can always jump on Discord and ask for help (create a `Support` thread).
|
||||
|
||||
Once you know which monitor is which, you want to look up their `serial_number_id` to use that on
|
||||
`display_index_preferences`, you can also use the `device_id`, it accepts both however there have been reported cases
|
||||
where the `device_id` changes after a restart while the `serial_number_id` doesn't.
|
||||
|
||||
So with the example above, we want the laptop to always use the configuration index 0 and the other monitor to use
|
||||
configuration index 1, so we map the configuration index number to the monitor `serial_number_id`/`device_id` like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "0",
|
||||
"1": "UEP174021562"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Again you could also have used the `device_id` like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "BOE0A1C-5&a2bea0b&0&UID512",
|
||||
"1": "VSC8C31-5&18560b1f&0&UID4356"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should add this `display_index_preferences` option to your `komorebi.json` file. If you find that something is
|
||||
not working as expected you can try to use the command `komorebic check`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> **When using multiple monitors it is recommended to always set the `display_index_preferences`. If you don't you might
|
||||
get some undefined behaviour.**
|
||||
|
||||
---
|
||||
|
||||
If you would like to run multiple instances of `komorebi-bar` to target different monitors, it is possible to do so
|
||||
using the `bar_configurations` array in your `komorebi.json` configuration file. You can refer to the
|
||||
[multiple-bar-instances](multiple-bar-instances.md) documentation.
|
||||
|
||||
In this case it is important to use `display_index_preferences`, because if you don't, and you have 3 or more monitors,
|
||||
disconnecting and reconnecting monitors may result in the bars for the monitors getting shifted around.
|
||||
|
||||
Consider this setup with 3 monitors (A, B and C):
|
||||
|
||||
```json
|
||||
// HOME_MONITOR_1_BAR.json
|
||||
{
|
||||
"monitor_index": 0
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// HOME_MONITOR_2_BAR.json
|
||||
{
|
||||
"monitor_index": 1
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// WORK_MONITOR_1_BAR.json
|
||||
{
|
||||
"monitor_index": 2
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "MONITOR_1_ID",
|
||||
"1": "MONITOR_2_ID",
|
||||
"2": "MONITOR_3_ID"
|
||||
},
|
||||
"bar_configurations": [
|
||||
// this bar uses "monitor_index": 0,
|
||||
"path/to/bar_config_1.json",
|
||||
// this bar uses "monitor_index": 1,
|
||||
"path/to/bar_config_2.json",
|
||||
// this bar uses "monitor_index": 2,
|
||||
"path/to/bar_config_3.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Komorebi uses an internal map to keep track of monitor to config indices, this map is called `monitor_usr_idx_map` it is
|
||||
an internal variable to komorebi that you don't need to do anything with, but you can see it with the [
|
||||
`komorebic state`](../cli/state.md) command (in case you need to debug something).
|
||||
|
||||
At first, komorebi will load all monitors and set the internal index map (`monitor_usr_idx_map`) as:
|
||||
|
||||
```json
|
||||
{
|
||||
// This is monitor A
|
||||
"0": 0,
|
||||
// This is monitor B
|
||||
"1": 1,
|
||||
// This is monitor C
|
||||
"2": 2
|
||||
}
|
||||
```
|
||||
|
||||
Which kind of seems unnecessary, but imagine that then you disconnect monitor B (or it goes to sleep). Then komorebi
|
||||
will only have 2 monitors with index 0 and 1, so the above map will be updated to this:
|
||||
|
||||
```jsonc
|
||||
[
|
||||
"0": 0, // This is monitor A
|
||||
"2": 1, // This is now monitor C, because monitor B disconnected
|
||||
]
|
||||
```
|
||||
|
||||
So now the bar intended to be for monitor B, which was looking for index "1" on that map, doesn't see it and knows it
|
||||
should be disabled. And the bar for monitor C looks at that map and knows that it's index "2" now maps to index 1 so it
|
||||
uses that index internally to get all the correct values about the monitor.
|
||||
|
||||
If you didn't have the `display_index_preferences` set, then when you disconnected monitor B, komorebi wouldn't know
|
||||
how to map the indices and would use default behaviour which would result in a map like this:
|
||||
|
||||
```json
|
||||
{
|
||||
// This is monitor A
|
||||
"0": 0,
|
||||
// This is monitor C, because monitor B disconnected. However the bars will think it is monitor B because it has index "1"
|
||||
"1": 1
|
||||
}
|
||||
```
|
||||
|
||||
# Multiple monitors on different machines
|
||||
|
||||
You can use the same `komorebi.json` to configure two different setups and then synchronize your config across machines.
|
||||
However, if you do this it is important to be aware of a few things.
|
||||
|
||||
Firstly, using `display_index_preferences` is required in this case.
|
||||
|
||||
You will need to get the `serial_number_id` or `device_id` of all the monitors of all your setups. With that information
|
||||
you would then set your config like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "HOME_MONITOR_1_ID",
|
||||
"1": "HOME_MONITOR_2_ID",
|
||||
"2": "WORK_MONITOR_1_ID",
|
||||
"3": "WORK_MONITOR_2_ID"
|
||||
},
|
||||
"monitors": [
|
||||
// HOME_MONITOR_1
|
||||
{
|
||||
"workspaces": [
|
||||
// ...
|
||||
]
|
||||
},
|
||||
// HOME_MONITOR_2
|
||||
{
|
||||
"workspaces": [
|
||||
// ...
|
||||
]
|
||||
},
|
||||
// WORK_MONITOR_1
|
||||
{
|
||||
"workspaces": [
|
||||
// ...
|
||||
]
|
||||
},
|
||||
// WORK_MONITOR_2
|
||||
{
|
||||
"workspaces": [
|
||||
// ...
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> *You can't use the same config on two different monitors, you have to make a duplicated config for each monitor!*
|
||||
|
||||
Then on the bar configs you need to set the bar's monitor index like this:
|
||||
|
||||
```json
|
||||
// HOME_MONITOR_1_BAR.json
|
||||
{
|
||||
"monitor_index": 0
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// HOME_MONITOR_2_BAR.json
|
||||
{
|
||||
"monitor_index": 1
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// WORK_MONITOR_1_BAR.json
|
||||
{
|
||||
"monitor_index": 2
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// WORK_MONITOR_2_BAR.json
|
||||
{
|
||||
"monitor_index": 3
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
Although you will only ever have 2 monitors connected at any one time, and they'll always have index 0 and 1, the
|
||||
above config will still work on both physical configurations.
|
||||
|
||||
This is because komorebi will apply the appropriate config to the loaded monitors and will create a map of the user
|
||||
index (the index defined in the user config) to the actual monitor index, and the bar will use that map to know if it
|
||||
should be enabled, and where it should be drawn.
|
||||
|
||||
# Windows Display Settings
|
||||
|
||||
In `Settings > System > Display > Multiple Displays`:
|
||||
|
||||
- Disable "Remember windows locations on monitor connection"
|
||||
- Enable "Minimize windows when a monitor is disconnected"
|
||||
|
||||
### Things to keep in mind
|
||||
|
||||
* If you are using a laptop connected to one monitor at work and a different one at home, the work monitor and the home
|
||||
monitor are considered different monitors by komorebi
|
||||
* When you disconnect from work, komorebi will keep the work monitor cached
|
||||
* You can still use a laptop alone without any monitor and if you need a window that was on the other monitor you can
|
||||
press the taskbar icon or use `alt + tab` to bring it to focus and that window will now be part of the laptop monitor
|
||||
* If you then reconnect the work monitor, the cached version will be applied with all its windows (except any window(s)
|
||||
you might have moved to another monitor)
|
||||
* If however, instead of reconnecting the work monitor, you connect the home monitor, then the work monitor will still
|
||||
remain cached, and komorebi will load the home monitor from the cache (if it exists)
|
||||
* Sometimes when you disconnect/reconnect a monitor the event might be missed by komorebi, meaning that Windows will
|
||||
show you both monitors but komorebi won't know about the existence of one of them
|
||||
* If you notice this type of weird behaviour, always run the [
|
||||
`komorebic monitor-info`](../cli/monitor-information.md)
|
||||
command and validate if one of the monitors is missing
|
||||
* To fix this you can try disconnecting and reconnecting the monitor again, or restarting komorebi
|
||||
@@ -8,12 +8,8 @@ configuration file.
|
||||
```json
|
||||
{
|
||||
"default_workspace_padding": 0,
|
||||
"default_container_padding": 0,
|
||||
"border_width": 0,
|
||||
"border_offset": -1
|
||||
"default_container_padding": -1,
|
||||
}
|
||||
```
|
||||
|
||||
A restart of `komorebi` is required after changing these settings.
|
||||
|
||||
[](https://www.youtube.com/watch?v=6QYLao953XE)
|
||||
|
||||
@@ -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>"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -186,6 +186,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" %}
|
||||
```
|
||||
|
||||
@@ -34,7 +34,7 @@ showcases the many awesome projects that exist in the `komorebi` ecosystem.
|
||||
|
||||
## Licensing for Personal Use
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 1.0.0 license](https://github.com/LGUG2Z/komorebi-license), which is a fork
|
||||
`komorebi` is licensed under the [Komorebi 2.0.0 license](https://github.com/LGUG2Z/komorebi-license), which is a fork
|
||||
of the [PolyForm Strict 1.0.0 license](https://polyformproject.org/licenses/strict/1.0.0). On a high level this means
|
||||
that you are free to do whatever you want with `komorebi` for personal use other than redistribution, or distribution of
|
||||
new works (i.e. hard-forks) based on the software.
|
||||
@@ -42,7 +42,7 @@ new works (i.e. hard-forks) based on the software.
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended either for personal use or for integration
|
||||
back upstream via pull requests.
|
||||
|
||||
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
|
||||
The [Komorebi 2.0.0 License](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
|
||||
i.e. using `komorebi` at work).
|
||||
|
||||
## Sponsorship for Personal Use
|
||||
|
||||
@@ -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.34/schema.bar.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.37/schema.bar.json",
|
||||
"monitor": 0,
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.37/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": [
|
||||
|
||||
@@ -132,3 +132,20 @@ running `komorebic stop` and `komorebic start`.
|
||||
We can see the _komorebi_ state is no longer associated with the previous
|
||||
device: `null`, suggesting an issue when the display resumes from a suspended
|
||||
state.
|
||||
|
||||
## Komorebi Bar does not render transparency on Nvidia GPUs
|
||||
|
||||
Users with Nvidia GPUs may have issues with transparency on the Komorebi Bar.
|
||||
|
||||
To solve this the user can do the following:
|
||||
|
||||
- Open the Nvidia Control Panel
|
||||
- On the left menu tree, under "3D Settings", select "Manage 3D Settings"
|
||||
- Select the "Program Settings" tab
|
||||
- Press the "Add" button and select "komorebi-bar"
|
||||
- Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
|
||||
- Change the setting to "Prefer compatibility"
|
||||
- At the bottom of the window select "Apply"
|
||||
- Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
|
||||
|
||||
This should resolve the issue and your Komorebi Bar should render with the proper transparency.
|
||||
|
||||
46
docs/usage/focusing-windows.md
Normal file
46
docs/usage/focusing-windows.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Focusing Windows
|
||||
|
||||
Windows can be focused in a direction (left, down, up, right) using the [`komorebic focus`](../cli/focus.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + h : komorebic focus left
|
||||
alt + j : komorebic focus down
|
||||
alt + k : komorebic focus up
|
||||
alt + l : komorebic focus right
|
||||
```
|
||||
|
||||
Windows can be focused in a cycle direction (previous, next) using the [`komorebic cycle-focus`](../cli/cycle-focus.md)
|
||||
command.
|
||||
|
||||
```
|
||||
# example showing you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-focus previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-focus next # oem_6 is ]
|
||||
```
|
||||
|
||||
It is possible to attempt to focus the first window, on any workspace, matching an exe using the [
|
||||
`komorebic eager-focus`](../cli/eager-focus.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
win + 1 : komorebic eager-focus firefox.exe
|
||||
```
|
||||
|
||||
The window at the largest tile can be focused using the [`komorebic promote-focus`](../cli/promote-focus.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + return : komorebic promote-focus
|
||||
```
|
||||
|
||||
The behaviour when attempting to call `komorebic focus` when at the left or right edge of a monitor is determined by
|
||||
the [`cross_boundary_behaviour`](https://komorebi.lgug2z.com/schema#cross_boundary_behaviour) configuration option.
|
||||
|
||||
When set to `Workspace`, the next workspace on the same monitor will be focused.
|
||||
|
||||
When set to `Monitor`, the focused workspace on the next monitor in the given direction will be focused.
|
||||
59
docs/usage/focusing-workspaces.md
Normal file
59
docs/usage/focusing-workspaces.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Focusing Workspaces
|
||||
|
||||
Workspaces on the focused monitor can be focused by their index using the [
|
||||
`komorebic focus-workspace`](../cli/focus-workspace.md) command.
|
||||
|
||||
If this command is called with an index for a workspace which does not exist, that workspace, and all workspace indexes
|
||||
required to get to that workspace, will be created.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + 1 : komorebic focus-workspace 0
|
||||
alt + 2 : komorebic focus-workspace 1
|
||||
alt + 3 : komorebic focus-workspace 2
|
||||
```
|
||||
|
||||
Workspaces on the focused monitor can be focused in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-workspace`](../cli/cycle-workspace.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-workspace previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-workspace next # oem_6 is ]
|
||||
```
|
||||
|
||||
Workspaces on other monitors can be focused by both the monitor index and the workspace index using the [
|
||||
`komorebic focus-monitor-workspace`](../cli/focus-monitor-workspace.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + 1 : komorebic focus-monitor-workspace 0 0
|
||||
alt + 2 : komorebic focus-monitor-workspace 0 1
|
||||
alt + 3 : komorebic focus-monitor-workspace 1 0
|
||||
```
|
||||
|
||||
Workspaces on any monitor can be focused by their name (given that all workspace names across all monitors are unique)
|
||||
using the [`komorebic focus-named-workspace`](../cli/focus-named-workspace.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + c : komorebic focus-named-workspace coding
|
||||
```
|
||||
|
||||
Workspaces on all monitors can be set to the same index (emulating single workspaces which span across all monitors)
|
||||
using the [`komorebic focus-workspaces`](../cli/focus-workspaces.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + 1 : komorebic focus-workspaces 0
|
||||
alt + 2 : komorebic focus-workspaces 1
|
||||
alt + 3 : komorebic focus-workspaces 2
|
||||
```
|
||||
|
||||
The last focused workspace on the focused monitor can be re-focused using the [
|
||||
`komorebic focus-last-workspace`](../cli/focus-last-workspace.md) command.
|
||||
59
docs/usage/moving-windows-across-workspaces.md
Normal file
59
docs/usage/moving-windows-across-workspaces.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Moving Windows Across Workspaces
|
||||
|
||||
Windows can be moved to another workspace on the focused monitor using the [
|
||||
`komorebic move-to-workspace`](../cli/move-to-workspace.md) command. This command will also move your focus to the
|
||||
target workspace.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + 1 : komorebic move-to-workspace 0
|
||||
alt + shift + 2 : komorebic move-to-workspace 1
|
||||
alt + shift + 3 : komorebic move-to-workspace 2
|
||||
```
|
||||
|
||||
Windows can be sent to another workspace on the focused monitor using the [
|
||||
`komorebic send-to-workspace`](../cli/send-to-workspace.md) command. This command will keep your focus on the origin
|
||||
workspace.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + 1 : komorebic send-to-workspace 0
|
||||
alt + shift + 2 : komorebic send-to-workspace 1
|
||||
alt + shift + 3 : komorebic send-to-workspace 2
|
||||
```
|
||||
|
||||
Windows can be moved to another workspace on the focused monitor in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-move-to-workspace`](../cli/cycle-move-to-workspace.md) command. This command will also move your focus
|
||||
to the target workspace.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-move-to-workspace previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-move-to-workspace next # oem_6 is ]
|
||||
```
|
||||
|
||||
Windows can be sent to another workspace on the focused monitor in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-move-to-workspace`](../cli/cycle-move-to-workspace.md) command. This command will keep your focus on
|
||||
the origin workspace.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-send-to-workspace previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-send-to-workspace next # oem_6 is ]
|
||||
```
|
||||
|
||||
Windows can be moved or sent to the focused workspace on a another monitor using the [
|
||||
`komorebic move-to-monitor`](../cli/move-to-monitor.md) and [`komorebic send-to-monitor`](../cli/send-to-monitor.md)
|
||||
commands.
|
||||
|
||||
Windows can be moved or sent to the focused workspace on a monitor in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-move-to-monitor`](../cli/cycle-move-to-monitor.md) and [
|
||||
`komorebic cycle-send-to-monitor`](../cli/cycle-send-to-monitor.md) commands.
|
||||
|
||||
Windows can be moved or sent to a named workspace on any monitor (given that all workspace names across all monitors are
|
||||
unique) using the [`komorebic move-to-named-workspace`](../cli/move-to-named-workspace.md) and [
|
||||
`komorebic send-to-named-workspace`](../cli/send-to-named-workspace.md) commands
|
||||
50
docs/usage/moving-windows.md
Normal file
50
docs/usage/moving-windows.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Moving Windows
|
||||
|
||||
Windows can be moved in a direction (left, down, up, right) using the [`komorebic move`](../cli/move.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + h : komorebic move left
|
||||
alt + shift + j : komorebic move down
|
||||
alt + shift + k : komorebic move up
|
||||
alt + shift + l : komorebic move right
|
||||
```
|
||||
|
||||
Windows can be moved in a cycle direction (previous, next) using the [`komorebic cycle-move`](../cli/cycle-move.md)
|
||||
command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-move previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-move next # oem_6 is ]
|
||||
```
|
||||
|
||||
The focused window can be moved to the largest tile using the [`komorebic promote`](../cli/promote.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + return : komorebic promote
|
||||
```
|
||||
|
||||
The behaviour when attempting to call `komorebic move` when at the left or right edge of a monitor is determined by
|
||||
the [`cross_boundary_behaviour`](https://komorebi.lgug2z.com/schema#cross_boundary_behaviour) configuration option.
|
||||
|
||||
When set to `Workspace`, the focused window will be moved to the next workspace on the focused monitor in the given
|
||||
direction
|
||||
|
||||
When set to `Monitor`, the focused window will be moved to the focused workspace on the next monitor in the given
|
||||
direction.
|
||||
|
||||
The behaviour when calling `komorebic move` with `cross_boundary_behaviour` set to `Monitor` can be further refined with
|
||||
the [`cross_monitor_move_behaviour`](https://komorebi.lgug2z.com/schema#cross_monitor_move_behaviour) configuration
|
||||
option.
|
||||
|
||||
When set to `Swap`, the focused window will be swapped with the window at the corresponding edge of the adjacent monitor
|
||||
|
||||
When set to `Insert`, the focused window will be inserted into the focused workspace on the adjacent monitor.
|
||||
|
||||
When set to `NoOp`, the focused window will not be moved across a monitor boundary, though focusing across monitor
|
||||
boundaries will continue to function.
|
||||
52
docs/usage/stacking-windows.md
Normal file
52
docs/usage/stacking-windows.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Stacking Windows
|
||||
|
||||
Windows can be stacked in a direction (left, down, up, right) using the [`komorebic stack`](../cli/stack.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + left : komorebic stack left
|
||||
alt + down : komorebic stack down
|
||||
alt + up : komorebic stack up
|
||||
alt + right : komorebic stack right
|
||||
```
|
||||
|
||||
Windows can be popped from a stack using the [`komorebic unstack`](../cli/unstack.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + oem_1 : komorebic unstack # oem_1 is ;
|
||||
```
|
||||
|
||||
Windows in a stack can be focused in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-stack`](../cli/cycle-stack.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + oem_4 : komorebic cycle-stack previous # oem_4 is [
|
||||
alt + oem_6 : komorebic cycle-stack next # oem_6 is ]
|
||||
```
|
||||
|
||||
Windows in a stack can have their positions in the stack moved in a cycle direction (previous, next) using the [
|
||||
`komorebic cycle-stack-index`](../cli/cycle-stack-index.md) command.
|
||||
|
||||
```
|
||||
# example showing how you might bind this command
|
||||
|
||||
alt + shift + oem_4 : komorebic cycle-stack-index previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-stack-index next # oem_6 is ]
|
||||
```
|
||||
|
||||
Windows in a stack can be focused by their index in the stack using the [
|
||||
`komorebic focus-stack-window`](../cli/focus-stack-window.md) command.
|
||||
|
||||
All windows on the focused workspace can be combined into a single stack using the [
|
||||
`komorebic stack-all`](../cli/stack-all.md) command.
|
||||
|
||||
All windows in a focused stack can be popped using the [`komorebic unstack-all`](../cli/unstack-all.md) command.
|
||||
|
||||
It is possible to tell the window manager to stack the next opened window on top of the currently focused window by
|
||||
using the [
|
||||
`komorebic toggle-workspace-window-container-behaviour`](../cli/toggle-workspace-window-container-behaviour.md) command.
|
||||
@@ -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 }
|
||||
|
||||
11
justfile
11
justfile
@@ -28,10 +28,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 +40,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 +52,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 +88,6 @@ 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 list --format json | jq 'del(.unlicensed)' > dependencies.json
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.35"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
komorebi-themes = { path = "../komorebi-themes", default-features = false }
|
||||
|
||||
chrono-tz = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
@@ -20,11 +21,13 @@ egui-phosphor = "0.9"
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
netdev = "0.32"
|
||||
lazy_static = { workspace = true }
|
||||
netdev = "0.34"
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
random_word = { version = "0.4", features = ["en"] }
|
||||
parking_lot = { workspace = true }
|
||||
random_word = { version = "0.5", features = ["en"] }
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
@@ -33,10 +36,12 @@ starship-battery = "0.10"
|
||||
sysinfo = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
which = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" }
|
||||
windows-icons-fallback = { package = "windows-icons", git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["dep:schemars"]
|
||||
schemars = ["dep:schemars", "komorebi-client/default", "komorebi-themes/default"]
|
||||
|
||||
@@ -4,16 +4,18 @@ use crate::config::KomobarTheme;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiNotificationState;
|
||||
use crate::process_hwnd;
|
||||
use crate::render::Color32Ext;
|
||||
use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::render::RenderExt;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widget::WidgetConfig;
|
||||
use crate::widgets::komorebi::Komorebi;
|
||||
use crate::widgets::komorebi::KomorebiNotificationState;
|
||||
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;
|
||||
@@ -36,6 +38,7 @@ use eframe::egui::Frame;
|
||||
use eframe::egui::Id;
|
||||
use eframe::egui::Layout;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::PointerButton;
|
||||
use eframe::egui::Rgba;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::TextStyle;
|
||||
@@ -43,21 +46,107 @@ use eframe::egui::Vec2;
|
||||
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_client::VirtualDesktopNotification;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use komorebi_themes::Base16Value;
|
||||
use komorebi_themes::Base16Wrapper;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::CatppuccinValue;
|
||||
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::Result;
|
||||
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;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
lazy_static! {
|
||||
static ref SESSION_STDIN: Mutex<Option<ChildStdin>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
fn start_powershell() -> 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() -> 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);
|
||||
}
|
||||
if let Err(e) = session_stdin.flush() {
|
||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
tracing::debug!("PowerShell session stopped");
|
||||
} else {
|
||||
tracing::debug!("PowerShell session already stopped");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_powershell(cmd: &str) -> 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);
|
||||
}
|
||||
|
||||
if let Err(e) = session_stdin.flush() {
|
||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(Error::new(
|
||||
ErrorKind::NotFound,
|
||||
"PowerShell session not started",
|
||||
))
|
||||
}
|
||||
|
||||
pub struct Komobar {
|
||||
pub hwnd: Option<isize>,
|
||||
pub monitor_index: Option<usize>,
|
||||
@@ -76,6 +165,18 @@ pub struct Komobar {
|
||||
pub size_rect: komorebi_client::Rect,
|
||||
pub work_area_offset: komorebi_client::Rect,
|
||||
applied_theme_on_first_frame: bool,
|
||||
mouse_follows_focus: bool,
|
||||
input_config: InputConfig,
|
||||
}
|
||||
|
||||
struct InputConfig {
|
||||
accumulated_scroll_delta: Vec2,
|
||||
act_on_vertical_scroll: bool,
|
||||
act_on_horizontal_scroll: bool,
|
||||
vertical_scroll_threshold: f32,
|
||||
horizontal_scroll_threshold: f32,
|
||||
vertical_scroll_max_threshold: f32,
|
||||
horizontal_scroll_max_threshold: f32,
|
||||
}
|
||||
|
||||
pub fn apply_theme(
|
||||
@@ -87,75 +188,86 @@ pub fn apply_theme(
|
||||
grouping: Option<Grouping>,
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
match theme {
|
||||
let (auto_select_fill, auto_select_text) = match theme {
|
||||
KomobarTheme::Catppuccin {
|
||||
name: catppuccin,
|
||||
accent: catppuccin_value,
|
||||
} => match catppuccin {
|
||||
Catppuccin::Frappe => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
auto_select_fill: catppuccin_auto_select_fill,
|
||||
auto_select_text: catppuccin_auto_select_text,
|
||||
} => {
|
||||
match catppuccin {
|
||||
Catppuccin::Frappe => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::FRAPPE.base);
|
||||
bg_color.replace(catppuccin_egui::FRAPPE.base);
|
||||
}
|
||||
Catppuccin::Latte => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::LATTE.base);
|
||||
}
|
||||
Catppuccin::Macchiato => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::MACCHIATO.base);
|
||||
}
|
||||
Catppuccin::Mocha => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::MOCHA.base);
|
||||
}
|
||||
}
|
||||
Catppuccin::Latte => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::LATTE.base);
|
||||
}
|
||||
Catppuccin::Macchiato => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::MACCHIATO.base);
|
||||
}
|
||||
Catppuccin::Mocha => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::MOCHA.base);
|
||||
}
|
||||
},
|
||||
(
|
||||
catppuccin_auto_select_fill.map(|c| c.color32(catppuccin.as_theme())),
|
||||
catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())),
|
||||
)
|
||||
}
|
||||
KomobarTheme::Base16 {
|
||||
name: base16,
|
||||
accent: base16_value,
|
||||
auto_select_fill: base16_auto_select_fill,
|
||||
auto_select_text: base16_auto_select_text,
|
||||
} => {
|
||||
ctx.set_style(base16.style());
|
||||
let base16_value = base16_value.unwrap_or_default();
|
||||
let accent = base16_value.color32(base16);
|
||||
let accent = base16_value.color32(Base16Wrapper::Base16(base16));
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
@@ -164,8 +276,46 @@ pub fn apply_theme(
|
||||
});
|
||||
|
||||
bg_color.replace(base16.background());
|
||||
|
||||
(
|
||||
base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Base16(base16))),
|
||||
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))),
|
||||
)
|
||||
}
|
||||
}
|
||||
KomobarTheme::Custom {
|
||||
colours,
|
||||
accent: base16_value,
|
||||
auto_select_fill: base16_auto_select_fill,
|
||||
auto_select_text: base16_auto_select_text,
|
||||
} => {
|
||||
let background = colours.background();
|
||||
ctx.set_style(colours.style());
|
||||
let base16_value = base16_value.unwrap_or_default();
|
||||
let accent = base16_value.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
});
|
||||
|
||||
bg_color.replace(background);
|
||||
|
||||
(
|
||||
base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))),
|
||||
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
AUTO_SELECT_FILL_COLOUR.store(
|
||||
auto_select_fill.map_or(0, |c| Colour::from(c).into()),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
AUTO_SELECT_TEXT_COLOUR.store(
|
||||
auto_select_text.map_or(0, |c| Colour::from(c).into()),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
// Apply transparency_alpha
|
||||
let theme_color = *bg_color.borrow();
|
||||
@@ -313,15 +463,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_state = self.komorebi_notification_state.as_ref().map(|state| {
|
||||
let state = state.borrow();
|
||||
(
|
||||
state.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
|
||||
state.mouse_follows_focus,
|
||||
)
|
||||
});
|
||||
|
||||
self.monitor_index = monitor_index;
|
||||
if let Some(state) = mapped_state {
|
||||
self.monitor_index = state.0;
|
||||
self.mouse_follows_focus = state.1;
|
||||
}
|
||||
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
|
||||
@@ -380,6 +534,36 @@ impl Komobar {
|
||||
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;
|
||||
@@ -431,11 +615,11 @@ impl Komobar {
|
||||
}
|
||||
|
||||
fn try_apply_theme(&mut self, ctx: &Context) {
|
||||
match self.config.theme {
|
||||
match &self.config.theme {
|
||||
Some(theme) => {
|
||||
apply_theme(
|
||||
ctx,
|
||||
theme,
|
||||
theme.clone(),
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
@@ -447,13 +631,16 @@ impl Komobar {
|
||||
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|
||||
|_| dirs::home_dir().expect("there is no home directory"),
|
||||
|home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
let home = home_path.replace_env();
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
);
|
||||
|
||||
home
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -463,6 +650,26 @@ 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),
|
||||
@@ -473,17 +680,6 @@ impl Komobar {
|
||||
self.render_config.clone(),
|
||||
);
|
||||
|
||||
let stack_accent = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name, stack_border, ..
|
||||
} => stack_border
|
||||
.unwrap_or(CatppuccinValue::Green)
|
||||
.color32(name.as_theme()),
|
||||
KomorebiTheme::Base16 {
|
||||
name, stack_border, ..
|
||||
} => stack_border.unwrap_or(Base16Value::Base0B).color32(name),
|
||||
};
|
||||
|
||||
if let Some(state) = &self.komorebi_notification_state {
|
||||
state.borrow_mut().stack_accent = Some(stack_accent);
|
||||
}
|
||||
@@ -541,6 +737,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);
|
||||
@@ -692,6 +898,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
|
||||
@@ -782,7 +1012,7 @@ impl eframe::App for Komobar {
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme,
|
||||
self.config.theme.clone(),
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
@@ -870,6 +1100,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
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::bar::exec_powershell;
|
||||
use crate::render::Grouping;
|
||||
use crate::widget::WidgetConfig;
|
||||
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.35`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.38`
|
||||
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)
|
||||
@@ -325,6 +330,147 @@ pub fn get_individual_spacing(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
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"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[serde(untagged)]
|
||||
Komorebi(KomorebiMouseMessage),
|
||||
/// Execute a custom command.
|
||||
/// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved.
|
||||
/// Example: `komorebic toggle-pause`
|
||||
#[serde(untagged)]
|
||||
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.into_iter()).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)?;
|
||||
@@ -373,7 +519,7 @@ impl From<Position> for Pos2 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
pub enum KomobarTheme {
|
||||
@@ -382,12 +528,24 @@ pub enum KomobarTheme {
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
auto_select_fill: Option<komorebi_themes::CatppuccinValue>,
|
||||
auto_select_text: Option<komorebi_themes::CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
name: komorebi_themes::Base16,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_fill: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_text: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
/// A custom Base16 theme
|
||||
Custom {
|
||||
/// Colours of the custom Base16 theme palette
|
||||
colours: Box<komorebi_themes::Base16ColourPalette>,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_fill: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_text: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -399,12 +557,26 @@ impl From<KomorebiTheme> for KomobarTheme {
|
||||
} => Self::Catppuccin {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
KomorebiTheme::Base16 {
|
||||
name, bar_accent, ..
|
||||
} => Self::Base16 {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
KomorebiTheme::Custom {
|
||||
colours,
|
||||
bar_accent,
|
||||
..
|
||||
} => Self::Custom {
|
||||
colours,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
mod bar;
|
||||
mod battery;
|
||||
mod config;
|
||||
mod cpu;
|
||||
mod date;
|
||||
mod keyboard;
|
||||
mod komorebi;
|
||||
mod komorebi_layout;
|
||||
mod media;
|
||||
mod memory;
|
||||
mod network;
|
||||
mod render;
|
||||
mod selected_frame;
|
||||
mod storage;
|
||||
mod time;
|
||||
mod ui;
|
||||
mod update;
|
||||
mod widget;
|
||||
mod widgets;
|
||||
|
||||
use crate::bar::Komobar;
|
||||
use crate::config::KomobarConfig;
|
||||
@@ -27,18 +15,17 @@ 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 std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
@@ -59,8 +46,8 @@ pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
|
||||
pub static BAR_HEIGHT: f32 = 50.0;
|
||||
pub static DEFAULT_PADDING: f32 = 10.0;
|
||||
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
@@ -73,6 +60,7 @@ struct Opts {
|
||||
fonts: bool,
|
||||
/// Path to a JSON or YAML configuration file
|
||||
#[clap(short, long)]
|
||||
#[clap(value_parser = replace_env_in_path)]
|
||||
config: Option<PathBuf>,
|
||||
/// Write an example komorebi.bar.json to disk
|
||||
#[clap(long)]
|
||||
@@ -167,13 +155,15 @@ fn main() -> color_eyre::Result<()> {
|
||||
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|
||||
|_| dirs::home_dir().expect("there is no home directory"),
|
||||
|home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
let home = home_path.replace_env();
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
|
||||
}
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
);
|
||||
|
||||
home
|
||||
},
|
||||
);
|
||||
|
||||
@@ -182,7 +172,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
|
||||
println!(
|
||||
"Example komorebi.bar.json file written to {}",
|
||||
home_dir.as_path().display()
|
||||
home_dir.display()
|
||||
);
|
||||
|
||||
std::process::exit(0);
|
||||
@@ -190,16 +180,11 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let default_config_path = home_dir.join("komorebi.bar.json");
|
||||
|
||||
let config_path = opts.config.map_or_else(
|
||||
|| {
|
||||
if !default_config_path.is_file() {
|
||||
None
|
||||
} else {
|
||||
Some(default_config_path.clone())
|
||||
}
|
||||
},
|
||||
Option::from,
|
||||
);
|
||||
let config_path = opts.config.or_else(|| {
|
||||
default_config_path
|
||||
.is_file()
|
||||
.then_some(default_config_path.clone())
|
||||
});
|
||||
|
||||
let mut config = match config_path {
|
||||
None => {
|
||||
@@ -209,17 +194,14 @@ fn main() -> color_eyre::Result<()> {
|
||||
std::fs::write(&default_config_path, komorebi_bar_json)?;
|
||||
tracing::info!(
|
||||
"created example configuration file: {}",
|
||||
default_config_path.as_path().display()
|
||||
default_config_path.display()
|
||||
);
|
||||
|
||||
KomobarConfig::read(&default_config_path)?
|
||||
}
|
||||
Some(ref config) => {
|
||||
if !opts.aliases {
|
||||
tracing::info!(
|
||||
"found configuration file: {}",
|
||||
config.as_path().to_string_lossy()
|
||||
);
|
||||
tracing::info!("found configuration file: {}", config.display());
|
||||
}
|
||||
|
||||
KomobarConfig::read(config)?
|
||||
@@ -319,10 +301,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
hotwatch.watch(config_path, move |event| match event.kind {
|
||||
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
|
||||
Ok(updated) => {
|
||||
tracing::info!(
|
||||
"configuration file updated: {}",
|
||||
config_path_cl.as_path().to_string_lossy()
|
||||
);
|
||||
tracing::info!("configuration file updated: {}", config_path_cl.display());
|
||||
|
||||
if let Err(error) = tx_config.send(updated) {
|
||||
tracing::error!("could not send configuration update to gui: {error}")
|
||||
@@ -349,7 +328,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let ctx_komorebi = cc.egui_ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
|
||||
let subscriber_name = format!("komorebi-bar-{}", random_word::get(random_word::Lang::En));
|
||||
|
||||
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
|
||||
filter_state_changes: true,
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use num_derive::FromPrimitive;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Networks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct NetworkConfig {
|
||||
/// Enable the Network widget
|
||||
pub enable: bool,
|
||||
/// Show total data transmitted
|
||||
pub show_total_data_transmitted: bool,
|
||||
/// Show network activity
|
||||
pub show_network_activity: bool,
|
||||
/// Show default interface
|
||||
pub show_default_interface: Option<bool>,
|
||||
/// Characters to reserve for network activity data
|
||||
pub network_activity_fill_characters: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<NetworkConfig> for Network {
|
||||
fn from(value: NetworkConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
show_total_activity: value.show_total_data_transmitted,
|
||||
show_activity: value.show_network_activity,
|
||||
show_default_interface: value.show_default_interface.unwrap_or(true),
|
||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
||||
default_interface: String::new(),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
network_activity_fill_characters: value
|
||||
.network_activity_fill_characters
|
||||
.unwrap_or_default(),
|
||||
last_state_total_activity: vec![],
|
||||
last_state_activity: vec![],
|
||||
last_updated_network_activity: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Network {
|
||||
pub enable: bool,
|
||||
pub show_total_activity: bool,
|
||||
pub show_activity: bool,
|
||||
pub show_default_interface: bool,
|
||||
networks_network_activity: Networks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
default_interface: String,
|
||||
last_state_total_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
last_updated_network_activity: Instant,
|
||||
network_activity_fill_characters: usize,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
fn default_interface(&mut self) {
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let mut activity = self.last_state_activity.clone();
|
||||
let mut total_activity = self.last_state_total_activity.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
activity.clear();
|
||||
total_activity.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
|
||||
self.networks_network_activity.refresh(true);
|
||||
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if self.show_activity {
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
Self::to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval,
|
||||
),
|
||||
Self::to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if self.show_total_activity {
|
||||
total_activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Total,
|
||||
Self::to_pretty_bytes(data.total_received(), 1),
|
||||
Self::to_pretty_bytes(data.total_transmitted(), 1),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_activity.clone_from(&activity);
|
||||
self.last_state_total_activity.clone_from(&total_activity);
|
||||
self.last_updated_network_activity = now;
|
||||
}
|
||||
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
fn reading_to_label(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
reading: NetworkReading,
|
||||
config: RenderConfig,
|
||||
) -> Label {
|
||||
let (text_down, text_up) = match self.label_prefix {
|
||||
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"{: >width$}/s ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"{: >width$}/s",
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("{} ", reading.received_text),
|
||||
reading.transmitted_text,
|
||||
),
|
||||
},
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"DOWN: {: >width$}/s ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"UP: {: >width$}/s",
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("\u{2211}DOWN: {}/s ", reading.received_text),
|
||||
format!("\u{2211}UP: {}/s", reading.transmitted_text),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
let icon_format = TextFormat::simple(
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
);
|
||||
let text_format = TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// icon
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
icon_format.font_id.clone(),
|
||||
icon_format.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job.append(
|
||||
&text_down,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
// icon
|
||||
layout_job.append(
|
||||
&match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ARROW_FAT_UP.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
0.0,
|
||||
icon_format.clone(),
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job.append(
|
||||
&text_up,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
Label::new(layout_job).selectable(false)
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
|
||||
let input = input_in_bytes as f32 / timespan_in_s as f32;
|
||||
let mut magnitude = input.log(1024f32) as u32;
|
||||
|
||||
// let the base unit be KiB
|
||||
if magnitude < 1 {
|
||||
magnitude = 1;
|
||||
}
|
||||
|
||||
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
|
||||
let result = input / ((1u64) << (magnitude * 10)) as f32;
|
||||
|
||||
match base {
|
||||
Some(DataUnit::B) => format!("{result:.1} B"),
|
||||
Some(unit) => format!("{result:.1} {unit}iB"),
|
||||
None => String::from("Unknown data unit"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Network {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
// widget spacing: make sure to use the same config to call the apply_on_widget function
|
||||
let mut render_config = config.clone();
|
||||
|
||||
if self.show_total_activity || self.show_activity {
|
||||
let (activity, total_activity) = self.network_activity();
|
||||
|
||||
if self.show_total_activity {
|
||||
for reading in total_activity {
|
||||
render_config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading, config.clone()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_activity {
|
||||
for reading in activity {
|
||||
render_config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading, config.clone()));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_default_interface {
|
||||
self.default_interface();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::WIFI_HIGH.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
self.default_interface.insert_str(0, "NET: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&self.default_interface,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// widget spacing: pass on the config that was use for calling the apply_on_widget function
|
||||
*config = render_config.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum NetworkReadingFormat {
|
||||
Speed = 0,
|
||||
Total = 1,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NetworkReading {
|
||||
pub format: NetworkReadingFormat,
|
||||
pub received_text: String,
|
||||
pub transmitted_text: String,
|
||||
}
|
||||
|
||||
impl NetworkReading {
|
||||
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
|
||||
NetworkReading {
|
||||
format,
|
||||
received_text: received,
|
||||
transmitted_text: transmitted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPrimitive)]
|
||||
enum DataUnit {
|
||||
B = 0,
|
||||
K = 1,
|
||||
M = 2,
|
||||
G = 3,
|
||||
T = 4,
|
||||
P = 5,
|
||||
E = 6,
|
||||
Z = 7,
|
||||
Y = 8,
|
||||
}
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
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;
|
||||
@@ -11,8 +13,11 @@ use eframe::egui::Margin;
|
||||
use eframe::egui::Shadow;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::Rgb;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::num::NonZeroU32;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
@@ -55,6 +60,10 @@ pub struct RenderConfig {
|
||||
pub icon_font_id: FontId,
|
||||
/// Show all icons on the workspace section of the Komorebi widget
|
||||
pub show_all_icons: bool,
|
||||
/// Background color of the selected frame
|
||||
pub auto_select_fill: Option<Color32>,
|
||||
/// Text color of the selected frame
|
||||
pub auto_select_text: Option<Color32>,
|
||||
}
|
||||
|
||||
pub trait RenderExt {
|
||||
@@ -108,6 +117,10 @@ impl RenderExt for &KomobarConfig {
|
||||
text_font_id,
|
||||
icon_font_id,
|
||||
show_all_icons,
|
||||
auto_select_fill: NonZeroU32::new(AUTO_SELECT_FILL_COLOUR.load(Ordering::SeqCst))
|
||||
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
|
||||
auto_select_text: NonZeroU32::new(AUTO_SELECT_TEXT_COLOUR.load(Ordering::SeqCst))
|
||||
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,6 +146,8 @@ impl RenderConfig {
|
||||
text_font_id: FontId::default(),
|
||||
icon_font_id: FontId::default(),
|
||||
show_all_icons: false,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,15 +10,29 @@ use eframe::egui::Ui;
|
||||
/// Same as SelectableLabel, but supports all content
|
||||
pub struct SelectableFrame {
|
||||
selected: bool,
|
||||
selected_fill: Option<Color32>,
|
||||
}
|
||||
|
||||
impl SelectableFrame {
|
||||
pub fn new(selected: bool) -> Self {
|
||||
Self { selected }
|
||||
Self {
|
||||
selected,
|
||||
selected_fill: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_auto(selected: bool, selected_fill: Option<Color32>) -> Self {
|
||||
Self {
|
||||
selected,
|
||||
selected_fill,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
|
||||
let Self { selected } = self;
|
||||
let Self {
|
||||
selected,
|
||||
selected_fill,
|
||||
} = self;
|
||||
|
||||
Frame::NONE
|
||||
.show(ui, |ui| {
|
||||
@@ -32,7 +46,16 @@ impl SelectableFrame {
|
||||
);
|
||||
|
||||
// since the stroke is drawn inside the frame, we always reserve space for it
|
||||
if response.hovered() || response.highlighted() || response.has_focus() {
|
||||
if selected && response.hovered() {
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
Frame::NONE
|
||||
.stroke(Stroke::new(1.0, visuals.bg_stroke.color))
|
||||
.corner_radius(visuals.corner_radius)
|
||||
.fill(selected_fill.unwrap_or(visuals.bg_fill))
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
} else if response.hovered() || response.highlighted() || response.has_focus() {
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
Frame::NONE
|
||||
@@ -47,7 +70,7 @@ impl SelectableFrame {
|
||||
Frame::NONE
|
||||
.stroke(Stroke::new(1.0, visuals.bg_fill))
|
||||
.corner_radius(visuals.corner_radius)
|
||||
.fill(visuals.bg_fill)
|
||||
.fill(selected_fill.unwrap_or(visuals.bg_fill))
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
} else {
|
||||
|
||||
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::vec2;
|
||||
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 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
@@ -28,6 +28,8 @@ pub struct BatteryConfig {
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Select when the current percentage is under this value [[1-100]]
|
||||
pub auto_select_under: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<BatteryConfig> for Battery {
|
||||
@@ -38,9 +40,10 @@ impl From<BatteryConfig> for Battery {
|
||||
enable: value.enable,
|
||||
hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false),
|
||||
manager: Manager::new().unwrap(),
|
||||
last_state: String::new(),
|
||||
last_state: None,
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
auto_select_under: value.auto_select_under.map(|u| u.clamp(1, 100)),
|
||||
state: BatteryState::Discharging,
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
@@ -52,6 +55,16 @@ impl From<BatteryConfig> for Battery {
|
||||
pub enum BatteryState {
|
||||
Charging,
|
||||
Discharging,
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
Warning,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct BatteryOutput {
|
||||
label: String,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
pub struct Battery {
|
||||
@@ -61,37 +74,53 @@ pub struct Battery {
|
||||
pub state: BatteryState,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_state: String,
|
||||
auto_select_under: Option<u8>,
|
||||
last_state: Option<BatteryOutput>,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Battery {
|
||||
fn output(&mut self) -> String {
|
||||
fn output(&mut self) -> Option<BatteryOutput> {
|
||||
let mut output = self.last_state.clone();
|
||||
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
output.clear();
|
||||
output = None;
|
||||
|
||||
if let Ok(mut batteries) = self.manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>();
|
||||
let percentage = first.state_of_charge().get::<percent>().round() as u8;
|
||||
|
||||
if percentage == 100.0 && self.hide_on_full_charge {
|
||||
output = String::new()
|
||||
if percentage == 100 && self.hide_on_full_charge {
|
||||
output = None
|
||||
} else {
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => self.state = BatteryState::Discharging,
|
||||
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,
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
output = match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
|
||||
|
||||
output = Some(BatteryOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => {
|
||||
format!("{percentage}%")
|
||||
}
|
||||
},
|
||||
selected,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,35 +137,43 @@ impl BarWidget for Battery {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
if let Some(output) = output {
|
||||
let emoji = match self.state {
|
||||
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
|
||||
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
|
||||
BatteryState::High => egui_phosphor::regular::BATTERY_HIGH,
|
||||
BatteryState::Medium => egui_phosphor::regular::BATTERY_MEDIUM,
|
||||
BatteryState::Low => egui_phosphor::regular::BATTERY_LOW,
|
||||
BatteryState::Warning => egui_phosphor::regular::BATTERY_WARNING,
|
||||
};
|
||||
|
||||
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
&output.label,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let auto_focus_fill = config.auto_select_fill;
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
@@ -25,6 +25,8 @@ pub struct CpuConfig {
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<CpuConfig> for Cpu {
|
||||
@@ -38,6 +40,7 @@ impl From<CpuConfig> for Cpu {
|
||||
),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
@@ -45,26 +48,38 @@ impl From<CpuConfig> for Cpu {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CpuOutput {
|
||||
label: String,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
pub struct Cpu {
|
||||
pub enable: bool,
|
||||
system: System,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select_over: Option<u8>,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
fn output(&mut self) -> String {
|
||||
fn output(&mut self) -> CpuOutput {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.system.refresh_cpu_usage();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let used = self.system.global_cpu_usage();
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
|
||||
let used = self.system.global_cpu_usage() as u8;
|
||||
let selected = self.auto_select_over.is_some_and(|o| used >= o);
|
||||
|
||||
CpuOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +88,9 @@ impl BarWidget for Cpu {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
if !output.label.is_empty() {
|
||||
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
@@ -82,23 +99,25 @@ impl BarWidget for Cpu {
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
&output.label,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let auto_focus_fill = config.auto_select_fill;
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
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;
|
||||
@@ -11,6 +13,8 @@ use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Custom format with additive modifiers for integer format specifiers
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -38,7 +42,7 @@ impl CustomModifiers {
|
||||
}
|
||||
|
||||
// get the strftime value of modifier
|
||||
let formatted_modifier = chrono::Local::now().format(modifier).to_string();
|
||||
let formatted_modifier = Local::now().format(modifier).to_string();
|
||||
|
||||
// find the gotten value in the original output
|
||||
if let Some(pos) = modified_output.find(&formatted_modifier) {
|
||||
@@ -64,14 +68,35 @@ pub struct DateConfig {
|
||||
pub format: DateFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// TimeZone (https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html)
|
||||
///
|
||||
/// Use a custom format to display additional information, i.e.:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "Date": {
|
||||
/// "enable": true,
|
||||
/// "format": { "Custom": "%D %Z (Tokyo)" },
|
||||
/// "timezone": "Asia/Tokyo"
|
||||
/// }
|
||||
///}
|
||||
/// ```
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
impl From<DateConfig> for Date {
|
||||
fn from(value: DateConfig) -> Self {
|
||||
let data_refresh_interval = 1;
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
timezone: value.timezone,
|
||||
data_refresh_interval,
|
||||
last_state: String::new(),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,19 +146,46 @@ pub struct Date {
|
||||
pub enable: bool,
|
||||
pub format: DateFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
timezone: Option<String>,
|
||||
data_refresh_interval: u64,
|
||||
last_state: String,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Date {
|
||||
fn output(&mut self) -> String {
|
||||
let formatted = chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string();
|
||||
let mut output = self.last_state.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
// if custom modifiers are used, apply them
|
||||
match &self.format {
|
||||
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
|
||||
_ => formatted,
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
let formatted = match &self.timezone {
|
||||
Some(timezone) => match timezone.parse::<Tz>() {
|
||||
Ok(tz) => Local::now()
|
||||
.with_timezone(&tz)
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
Err(_) => format!("Invalid timezone: {}", timezone),
|
||||
},
|
||||
None => Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
// if custom modifiers are used, apply them
|
||||
output = match &self.format {
|
||||
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
|
||||
_ => formatted,
|
||||
};
|
||||
|
||||
self.last_state.clone_from(&output);
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
2
komorebi-bar/src/keyboard.rs → komorebi-bar/src/widgets/keyboard.rs
Executable file → Normal file
2
komorebi-bar/src/keyboard.rs → komorebi-bar/src/widgets/keyboard.rs
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
@@ -1,19 +1,20 @@
|
||||
use super::ImageIcon;
|
||||
use crate::bar::apply_theme;
|
||||
use crate::config::DisplayFormat;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::config::WorkspacesDisplayFormat;
|
||||
use crate::komorebi_layout::KomorebiLayout;
|
||||
use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::ICON_CACHE;
|
||||
use crate::widgets::komorebi_layout::KomorebiLayout;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_INDEX;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::vec2;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ColorImage;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::Frame;
|
||||
@@ -24,11 +25,9 @@ use eframe::egui::RichText;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::TextureHandle;
|
||||
use eframe::egui::TextureOptions;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::Container;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::PathExt;
|
||||
@@ -55,8 +54,11 @@ pub struct KomorebiConfig {
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
/// Configure the Workspace Layer widget
|
||||
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
|
||||
/// Configure the Focused Window widget
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
/// Configure the Focused Container widget
|
||||
#[serde(alias = "focused_window")]
|
||||
pub focused_container: Option<KomorebiFocusedContainerConfig>,
|
||||
/// Configure the Locked Container widget
|
||||
pub locked_container: Option<KomorebiLockedContainerConfig>,
|
||||
/// Configure the Configuration Switcher widget
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
@@ -96,15 +98,26 @@ pub struct KomorebiWorkspaceLayerConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiFocusedWindowConfig {
|
||||
/// Enable the Komorebi Focused Window widget
|
||||
pub struct KomorebiFocusedContainerConfig {
|
||||
/// Enable the Komorebi Focused Container widget
|
||||
pub enable: bool,
|
||||
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused window)
|
||||
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused container)
|
||||
pub show_icon: Option<bool>,
|
||||
/// Display format of the currently focused window
|
||||
/// Display format of the currently focused container
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiLockedContainerConfig {
|
||||
/// Enable the Komorebi Locked Container widget
|
||||
pub enable: bool,
|
||||
/// Display format of the current locked state
|
||||
pub display: Option<DisplayFormat>,
|
||||
/// Show the widget event if the layer is unlocked
|
||||
pub show_when_unlocked: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiConfigurationSwitcherConfig {
|
||||
@@ -140,15 +153,19 @@ impl From<&KomorebiConfig> for Komorebi {
|
||||
.unwrap_or_default(),
|
||||
mouse_follows_focus: true,
|
||||
work_area_offset: None,
|
||||
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
|
||||
focused_container_information: (
|
||||
false,
|
||||
KomorebiNotificationStateContainerInformation::EMPTY,
|
||||
),
|
||||
stack_accent: None,
|
||||
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
|
||||
monitor_usr_idx_map: HashMap::new(),
|
||||
})),
|
||||
workspaces: value.workspaces,
|
||||
layout: value.layout.clone(),
|
||||
focused_window: value.focused_window,
|
||||
focused_container: value.focused_container,
|
||||
workspace_layer: value.workspace_layer,
|
||||
locked_container: value.locked_container,
|
||||
configuration_switcher,
|
||||
}
|
||||
}
|
||||
@@ -159,8 +176,9 @@ pub struct Komorebi {
|
||||
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
|
||||
pub workspaces: Option<KomorebiWorkspacesConfig>,
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
pub focused_container: Option<KomorebiFocusedContainerConfig>,
|
||||
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
|
||||
pub locked_container: Option<KomorebiLockedContainerConfig>,
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
|
||||
@@ -178,9 +196,10 @@ impl BarWidget for Komorebi {
|
||||
let format = workspaces.display.unwrap_or(DisplayFormat::Text.into());
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for (i, (ws, containers, _)) in
|
||||
for (i, (ws, containers, _, should_show)) in
|
||||
komorebi_notification_state.workspaces.iter().enumerate()
|
||||
{
|
||||
if *should_show {
|
||||
let is_selected = komorebi_notification_state.selected_workspace.eq(ws);
|
||||
|
||||
if SelectableFrame::new(
|
||||
@@ -210,7 +229,7 @@ impl BarWidget for Komorebi {
|
||||
for (is_focused, container) in containers {
|
||||
for icon in container.icons.iter().flatten().collect::<Vec<_>>() {
|
||||
ui.add(
|
||||
Image::from(&img_to_texture(ctx, icon))
|
||||
Image::from(&icon.texture(ctx))
|
||||
.maintain_aspect_ratio(true)
|
||||
.fit_to_exact_size(if *is_focused { icon_size } else { text_size }),
|
||||
);
|
||||
@@ -269,7 +288,6 @@ impl BarWidget for Komorebi {
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
),
|
||||
SocketMessage::RetileWithResizeDimensions,
|
||||
SocketMessage::MouseFollowsFocus(true),
|
||||
])
|
||||
.is_err()
|
||||
@@ -278,7 +296,6 @@ impl BarWidget for Komorebi {
|
||||
"could not send the following batch of messages to komorebi:\n
|
||||
MouseFollowsFocus(false)\n
|
||||
FocusMonitorWorkspaceNumber({}, {})\n
|
||||
RetileWithResizeDimensions
|
||||
MouseFollowsFocus(true)\n",
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
@@ -289,19 +306,18 @@ impl BarWidget for Komorebi {
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
),
|
||||
SocketMessage::RetileWithResizeDimensions,
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send the following batch of messages to komorebi:\n
|
||||
FocusMonitorWorkspaceNumber({}, {})\n
|
||||
RetileWithResizeDimensions",
|
||||
FocusMonitorWorkspaceNumber({}, {})\n",
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -318,7 +334,7 @@ impl BarWidget for Komorebi {
|
||||
.workspaces
|
||||
.iter()
|
||||
.find(|o| komorebi_notification_state.selected_workspace.eq(&o.0))
|
||||
.map(|(_, _, layer)| layer);
|
||||
.map(|(_, _, layer, _)| layer);
|
||||
|
||||
if let Some(layer) = layer {
|
||||
if (layer_config.show_when_tiling.unwrap_or_default()
|
||||
@@ -335,7 +351,7 @@ impl BarWidget for Komorebi {
|
||||
if matches!(layer, WorkspaceLayer::Tiling) {
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(size, Sense::hover());
|
||||
let color = ui.style().visuals.text_color();
|
||||
let color = ctx.style().visuals.selection.stroke.color;
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
let mut rect = response.rect;
|
||||
let corner =
|
||||
@@ -365,7 +381,7 @@ impl BarWidget for Komorebi {
|
||||
} else {
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(size, Sense::hover());
|
||||
let color = ui.style().visuals.text_color();
|
||||
let color = ctx.style().visuals.selection.stroke.color;
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
let mut rect = response.rect;
|
||||
let corner =
|
||||
@@ -403,6 +419,7 @@ impl BarWidget for Komorebi {
|
||||
|
||||
if layer_frame.clicked()
|
||||
&& komorebi_client::send_batch([
|
||||
SocketMessage::FocusMonitorAtCursor,
|
||||
SocketMessage::MouseFollowsFocus(false),
|
||||
SocketMessage::ToggleWorkspaceLayer,
|
||||
SocketMessage::MouseFollowsFocus(
|
||||
@@ -447,72 +464,109 @@ impl BarWidget for Komorebi {
|
||||
for (name, location) in configuration_switcher.configurations.iter() {
|
||||
let path = PathBuf::from(location);
|
||||
if path.is_file() {
|
||||
config.apply_on_widget(false, ui,|ui|{
|
||||
if SelectableFrame::new(false).show(ui, |ui|{
|
||||
ui.add(Label::new(name).selectable(false))
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
|
||||
let mut proceed = true;
|
||||
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
|
||||
canonicalized,
|
||||
))
|
||||
.is_err()
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(name).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: ReplaceConfiguration"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
let canonicalized =
|
||||
dunce::canonicalize(path.clone()).unwrap_or(path);
|
||||
|
||||
if let Some(rect) = komorebi_notification_state.work_area_offset {
|
||||
if proceed {
|
||||
match komorebi_client::send_query(&SocketMessage::Query(
|
||||
komorebi_client::StateQuery::FocusedMonitorIndex,
|
||||
)) {
|
||||
Ok(idx) => {
|
||||
if let Ok(monitor_idx) = idx.parse::<usize>() {
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MonitorWorkAreaOffset"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: Query"
|
||||
);
|
||||
}
|
||||
}
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::ReplaceConfiguration(canonicalized),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: ReplaceConfiguration"
|
||||
);
|
||||
}
|
||||
}
|
||||
}});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(focused_window) = self.focused_window {
|
||||
if focused_window.enable {
|
||||
if let Some(locked_container_config) = self.locked_container {
|
||||
if locked_container_config.enable {
|
||||
let is_locked = komorebi_notification_state.focused_container_information.0;
|
||||
|
||||
if locked_container_config
|
||||
.show_when_unlocked
|
||||
.unwrap_or_default()
|
||||
|| is_locked
|
||||
{
|
||||
let titles = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.1
|
||||
.titles;
|
||||
|
||||
if !titles.is_empty() {
|
||||
let display_format = locked_container_config
|
||||
.display
|
||||
.unwrap_or(DisplayFormat::Text);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
if display_format != DisplayFormat::Text {
|
||||
if is_locked {
|
||||
egui_phosphor::regular::LOCK_KEY.to_string()
|
||||
} else {
|
||||
egui_phosphor::regular::LOCK_SIMPLE_OPEN.to_string()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if display_format != DisplayFormat::Icon {
|
||||
layout_job.append(
|
||||
if is_locked { "Locked" } else { "Unlocked" },
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
&& komorebi_client::send_batch([
|
||||
SocketMessage::FocusMonitorAtCursor,
|
||||
SocketMessage::ToggleLock,
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send ToggleLock");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(focused_container_config) = self.focused_container {
|
||||
if focused_container_config.enable {
|
||||
let titles = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.1
|
||||
.titles;
|
||||
|
||||
if !titles.is_empty() {
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
let icons = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.focused_container_information.1
|
||||
.icons;
|
||||
let focused_window_idx = komorebi_notification_state
|
||||
.focused_container_information
|
||||
.focused_container_information.1
|
||||
.focused_window_idx;
|
||||
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
@@ -520,13 +574,13 @@ impl BarWidget for Komorebi {
|
||||
|
||||
for (i, (title, icon)) in iter.enumerate() {
|
||||
let selected = i == focused_window_idx && len != 1;
|
||||
let text_color = if selected { ctx.style().visuals.selection.stroke.color} else { ui.style().visuals.text_color() };
|
||||
let text_color = if selected { ctx.style().visuals.selection.stroke.color } else { ui.style().visuals.text_color() };
|
||||
|
||||
if SelectableFrame::new(selected)
|
||||
.show(ui, |ui| {
|
||||
// handle legacy setting
|
||||
let format = focused_window.display.unwrap_or(
|
||||
if focused_window.show_icon.unwrap_or(false) {
|
||||
let format = focused_container_config.display.unwrap_or(
|
||||
if focused_container_config.show_icon.unwrap_or(false) {
|
||||
DisplayFormat::IconAndText
|
||||
} else {
|
||||
DisplayFormat::Text
|
||||
@@ -546,7 +600,7 @@ impl BarWidget for Komorebi {
|
||||
))
|
||||
.show(ui, |ui| {
|
||||
let response = ui.add(
|
||||
Image::from(&img_to_texture(ctx, img))
|
||||
Image::from(&img.texture(ctx) )
|
||||
.maintain_aspect_ratio(true)
|
||||
.fit_to_exact_size(icon_size),
|
||||
);
|
||||
@@ -612,13 +666,6 @@ impl BarWidget for Komorebi {
|
||||
}
|
||||
}
|
||||
|
||||
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
|
||||
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||
let pixels = rgba_image.as_flat_samples();
|
||||
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
|
||||
ctx.load_texture("icon", color_image, TextureOptions::default())
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationState {
|
||||
@@ -626,9 +673,10 @@ pub struct KomorebiNotificationState {
|
||||
String,
|
||||
Vec<(bool, KomorebiNotificationStateContainerInformation)>,
|
||||
WorkspaceLayer,
|
||||
bool,
|
||||
)>,
|
||||
pub selected_workspace: String,
|
||||
pub focused_container_information: KomorebiNotificationStateContainerInformation,
|
||||
pub focused_container_information: (bool, KomorebiNotificationStateContainerInformation),
|
||||
pub layout: KomorebiLayout,
|
||||
pub hide_empty_workspaces: bool,
|
||||
pub mouse_follows_focus: bool,
|
||||
@@ -659,6 +707,7 @@ impl KomorebiNotificationState {
|
||||
let show_all_icons = render_config.borrow().show_all_icons;
|
||||
|
||||
match notification.event {
|
||||
NotificationEvent::VirtualDesktop(_) => {}
|
||||
NotificationEvent::WindowManager(_) => {}
|
||||
NotificationEvent::Monitor(_) => {}
|
||||
NotificationEvent::Socket(message) => match message {
|
||||
@@ -694,7 +743,7 @@ impl KomorebiNotificationState {
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
KomobarTheme::from(*theme),
|
||||
bg_color,
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
@@ -741,42 +790,41 @@ impl KomorebiNotificationState {
|
||||
true
|
||||
};
|
||||
|
||||
if should_show {
|
||||
workspaces.push((
|
||||
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
|
||||
if show_all_icons {
|
||||
let mut containers = vec![];
|
||||
let mut has_monocle = false;
|
||||
workspaces.push((
|
||||
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
|
||||
if show_all_icons {
|
||||
let mut containers = vec![];
|
||||
let mut has_monocle = false;
|
||||
|
||||
// add monocle container
|
||||
if let Some(container) = ws.monocle_container() {
|
||||
containers.push((true, container.into()));
|
||||
has_monocle = true;
|
||||
}
|
||||
// add monocle container
|
||||
if let Some(container) = ws.monocle_container() {
|
||||
containers.push((true, container.into()));
|
||||
has_monocle = true;
|
||||
}
|
||||
|
||||
// add all tiled windows
|
||||
for (i, container) in ws.containers().iter().enumerate() {
|
||||
containers.push((
|
||||
!has_monocle && i == ws.focused_container_idx(),
|
||||
container.into(),
|
||||
));
|
||||
}
|
||||
// add all tiled windows
|
||||
for (i, container) in ws.containers().iter().enumerate() {
|
||||
containers.push((
|
||||
!has_monocle && i == ws.focused_container_idx(),
|
||||
container.into(),
|
||||
));
|
||||
}
|
||||
|
||||
// add all floating windows
|
||||
for floating_window in ws.floating_windows() {
|
||||
containers.push((
|
||||
!has_monocle && floating_window.is_focused(),
|
||||
floating_window.into(),
|
||||
));
|
||||
}
|
||||
// add all floating windows
|
||||
for floating_window in ws.floating_windows() {
|
||||
containers.push((
|
||||
!has_monocle && floating_window.is_focused(),
|
||||
floating_window.into(),
|
||||
));
|
||||
}
|
||||
|
||||
containers
|
||||
} else {
|
||||
vec![(true, ws.into())]
|
||||
},
|
||||
ws.layer().to_owned(),
|
||||
));
|
||||
}
|
||||
containers
|
||||
} else {
|
||||
vec![(true, ws.into())]
|
||||
},
|
||||
ws.layer().to_owned(),
|
||||
should_show,
|
||||
));
|
||||
}
|
||||
|
||||
self.workspaces = workspaces;
|
||||
@@ -797,14 +845,19 @@ impl KomorebiNotificationState {
|
||||
};
|
||||
}
|
||||
|
||||
self.focused_container_information = (&monitor.workspaces()[focused_workspace_idx]).into();
|
||||
let focused_workspace = &monitor.workspaces()[focused_workspace_idx];
|
||||
let is_focused = focused_workspace
|
||||
.locked_containers()
|
||||
.contains(&focused_workspace.focused_container_idx());
|
||||
|
||||
self.focused_container_information = (is_focused, focused_workspace.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationStateContainerInformation {
|
||||
pub titles: Vec<String>,
|
||||
pub icons: Vec<Option<RgbaImage>>,
|
||||
pub icons: Vec<Option<ImageIcon>>,
|
||||
pub focused_window_idx: usize,
|
||||
}
|
||||
|
||||
@@ -831,29 +884,17 @@ impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
|
||||
impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Container) -> Self {
|
||||
let windows = value.windows().iter().collect::<Vec<_>>();
|
||||
let mut icons = vec![];
|
||||
|
||||
for window in windows {
|
||||
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
||||
let mut update_cache = false;
|
||||
let exe = window.exe().unwrap_or_default();
|
||||
|
||||
match icon_cache.get(&exe) {
|
||||
None => {
|
||||
icons.push(windows_icons::get_icon_by_process_id(window.process_id()));
|
||||
update_cache = true;
|
||||
}
|
||||
Some(icon) => {
|
||||
icons.push(Some(icon.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if update_cache {
|
||||
if let Some(Some(icon)) = icons.last() {
|
||||
icon_cache.insert(exe, icon.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let icons = windows
|
||||
.iter()
|
||||
.map(|window| {
|
||||
ImageIcon::try_load(window.hwnd, || {
|
||||
windows_icons::get_icon_by_hwnd(window.hwnd).or_else(|| {
|
||||
windows_icons_fallback::get_icon_by_process_id(window.process_id())
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self {
|
||||
titles: value
|
||||
@@ -869,30 +910,14 @@ impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
||||
|
||||
impl From<&Window> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Window) -> Self {
|
||||
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
||||
let mut update_cache = false;
|
||||
let mut icons = vec![];
|
||||
let exe = value.exe().unwrap_or_default();
|
||||
|
||||
match icon_cache.get(&exe) {
|
||||
None => {
|
||||
icons.push(windows_icons::get_icon_by_process_id(value.process_id()));
|
||||
update_cache = true;
|
||||
}
|
||||
Some(icon) => {
|
||||
icons.push(Some(icon.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if update_cache {
|
||||
if let Some(Some(icon)) = icons.last() {
|
||||
icon_cache.insert(exe, icon.clone());
|
||||
}
|
||||
}
|
||||
let icons = ImageIcon::try_load(value.hwnd, || {
|
||||
windows_icons::get_icon_by_hwnd(value.hwnd)
|
||||
.or_else(|| windows_icons_fallback::get_icon_by_process_id(value.process_id()))
|
||||
});
|
||||
|
||||
Self {
|
||||
titles: vec![value.title().unwrap_or_default()],
|
||||
icons,
|
||||
icons: vec![icons],
|
||||
focused_window_idx: 0,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config::DisplayFormat;
|
||||
use crate::komorebi::KomorebiLayoutConfig;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::komorebi::KomorebiLayoutConfig;
|
||||
use eframe::egui::vec2;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
@@ -105,12 +105,22 @@ impl KomorebiLayout {
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Monocle => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() {
|
||||
if komorebi_client::send_batch([
|
||||
SocketMessage::FocusMonitorAtCursor,
|
||||
SocketMessage::ToggleMonocle,
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: ToggleMonocle");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Floating => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() {
|
||||
if komorebi_client::send_batch([
|
||||
SocketMessage::FocusMonitorAtCursor,
|
||||
SocketMessage::ToggleTiling,
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: ToggleTiling");
|
||||
}
|
||||
}
|
||||
@@ -178,6 +188,12 @@ impl KomorebiLayout {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
// TODO: @CtByte can you think of a nice icon to draw here?
|
||||
komorebi_client::DefaultLayout::Scrolling => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
},
|
||||
KomorebiLayout::Monocle => {}
|
||||
KomorebiLayout::Floating => {
|
||||
@@ -241,7 +257,7 @@ impl KomorebiLayout {
|
||||
let layout_frame = SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
|
||||
self.show_icon(false, font_id.clone(), ctx, ui);
|
||||
self.show_icon(true, font_id.clone(), ctx, ui);
|
||||
}
|
||||
|
||||
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
@@ -25,6 +25,8 @@ pub struct MemoryConfig {
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<MemoryConfig> for Memory {
|
||||
@@ -38,6 +40,7 @@ impl From<MemoryConfig> for Memory {
|
||||
),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
@@ -45,16 +48,23 @@ impl From<MemoryConfig> for Memory {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct MemoryOutput {
|
||||
label: String,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
pub struct Memory {
|
||||
pub enable: bool,
|
||||
system: System,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select_over: Option<u8>,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
fn output(&mut self) -> String {
|
||||
fn output(&mut self) -> MemoryOutput {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.system.refresh_memory();
|
||||
@@ -63,11 +73,17 @@ impl Memory {
|
||||
|
||||
let used = self.system.used_memory();
|
||||
let total = self.system.total_memory();
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
let usage = ((used * 100) / total) as u8;
|
||||
let selected = self.auto_select_over.is_some_and(|o| usage >= o);
|
||||
|
||||
MemoryOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", usage)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +92,9 @@ impl BarWidget for Memory {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
if !output.label.is_empty() {
|
||||
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
@@ -85,23 +103,25 @@ impl BarWidget for Memory {
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
&output.label,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let auto_focus_fill = config.auto_select_fill;
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
173
komorebi-bar/src/widgets/mod.rs
Normal file
173
komorebi-bar/src/widgets/mod.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use eframe::egui::ColorImage;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::TextureHandle;
|
||||
use eframe::egui::TextureOptions;
|
||||
use image::RgbaImage;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::RwLock;
|
||||
|
||||
pub mod applications;
|
||||
pub mod battery;
|
||||
pub mod cpu;
|
||||
pub mod date;
|
||||
pub mod keyboard;
|
||||
pub mod komorebi;
|
||||
mod komorebi_layout;
|
||||
pub mod media;
|
||||
pub mod memory;
|
||||
pub mod network;
|
||||
pub mod storage;
|
||||
pub mod time;
|
||||
pub mod update;
|
||||
pub mod widget;
|
||||
|
||||
/// Global cache for icon images and their associated GPU textures.
|
||||
pub static ICONS_CACHE: IconsCache = IconsCache::new();
|
||||
|
||||
/// In-memory cache for icon images and their associated GPU textures.
|
||||
///
|
||||
/// Stores raw [`ColorImage`]s and [`TextureHandle`]s keyed by [`ImageIconId`].
|
||||
/// Texture entries are context-dependent and automatically invalidated when the [`Context`] changes.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct IconsCache {
|
||||
textures: LazyLock<RwLock<(Option<Context>, HashMap<ImageIconId, TextureHandle>)>>,
|
||||
images: LazyLock<RwLock<HashMap<ImageIconId, Arc<ColorImage>>>>,
|
||||
}
|
||||
|
||||
impl IconsCache {
|
||||
/// Creates a new empty IconsCache instance.
|
||||
#[inline]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
textures: LazyLock::new(|| RwLock::new((None, HashMap::new()))),
|
||||
images: LazyLock::new(|| RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves or creates a texture handle for the given icon ID and image.
|
||||
///
|
||||
/// If a texture for the given ID already exists for the current [`Context`], it is reused.
|
||||
/// Otherwise, a new texture is created, inserted into the cache, and returned.
|
||||
/// The cache is reset if the [`Context`] has changed.
|
||||
#[inline]
|
||||
pub fn texture(&self, ctx: &Context, id: &ImageIconId, img: &Arc<ColorImage>) -> TextureHandle {
|
||||
if let Some(texture) = self.get_texture(ctx, id) {
|
||||
return texture;
|
||||
}
|
||||
let texture_handle = ctx.load_texture("icon", img.clone(), TextureOptions::default());
|
||||
self.insert_texture(ctx, id.clone(), texture_handle.clone());
|
||||
texture_handle
|
||||
}
|
||||
|
||||
/// Returns the cached texture for the given icon ID if it exists and matches the current [`Context`].
|
||||
pub fn get_texture(&self, ctx: &Context, id: &ImageIconId) -> Option<TextureHandle> {
|
||||
let textures_lock = self.textures.read().unwrap();
|
||||
if textures_lock.0.as_ref() == Some(ctx) {
|
||||
return textures_lock.1.get(id).cloned();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Inserts a texture handle, resetting the cache if the [`Context`] has changed.
|
||||
pub fn insert_texture(&self, ctx: &Context, id: ImageIconId, texture: TextureHandle) {
|
||||
let mut textures_lock = self.textures.write().unwrap();
|
||||
|
||||
if textures_lock.0.as_ref() != Some(ctx) {
|
||||
textures_lock.0 = Some(ctx.clone());
|
||||
textures_lock.1.clear();
|
||||
}
|
||||
|
||||
textures_lock.1.insert(id, texture);
|
||||
}
|
||||
|
||||
/// Returns the cached image for the given icon ID, if available.
|
||||
pub fn get_image(&self, id: &ImageIconId) -> Option<Arc<ColorImage>> {
|
||||
self.images.read().unwrap().get(id).cloned()
|
||||
}
|
||||
|
||||
/// Caches a raw [`ColorImage`] associated with the given icon ID.
|
||||
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
|
||||
self.images.write().unwrap().insert(id, image);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
|
||||
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||
let pixels = rgba_image.as_flat_samples();
|
||||
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
|
||||
}
|
||||
|
||||
/// Represents an image-based icon with a unique ID and pixel data.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImageIcon {
|
||||
/// Unique identifier for the image icon, used for texture caching.
|
||||
pub id: ImageIconId,
|
||||
/// Shared pixel data of the icon in `ColorImage` format.
|
||||
pub image: Arc<ColorImage>,
|
||||
}
|
||||
|
||||
impl ImageIcon {
|
||||
/// Creates a new [`ImageIcon`] from the given ID and image data.
|
||||
#[inline]
|
||||
pub fn new(id: ImageIconId, image: Arc<ColorImage>) -> Self {
|
||||
Self { id, image }
|
||||
}
|
||||
|
||||
/// Loads an [`ImageIcon`] from [`ICONS_CACHE`] or calls `loader` if not cached.
|
||||
/// The loaded image is converted to a [`ColorImage`], cached, and returned.
|
||||
#[inline]
|
||||
pub fn try_load<F, I>(id: impl Into<ImageIconId>, loader: F) -> Option<Self>
|
||||
where
|
||||
F: FnOnce() -> Option<I>,
|
||||
I: Into<RgbaImage>,
|
||||
{
|
||||
let id = id.into();
|
||||
let image = ICONS_CACHE.get_image(&id).or_else(|| {
|
||||
let img = loader()?;
|
||||
let img = Arc::new(rgba_to_color_image(&img.into()));
|
||||
ICONS_CACHE.insert_image(id.clone(), img.clone());
|
||||
Some(img)
|
||||
})?;
|
||||
|
||||
Some(ImageIcon::new(id, image))
|
||||
}
|
||||
|
||||
/// Returns a texture handle for the icon, using the given [`Context`].
|
||||
///
|
||||
/// If the texture is already cached in [`ICONS_CACHE`], it is reused.
|
||||
/// Otherwise, a new texture is created from the [`ColorImage`] and cached.
|
||||
#[inline]
|
||||
pub fn texture(&self, ctx: &Context) -> TextureHandle {
|
||||
ICONS_CACHE.texture(ctx, &self.id, &self.image)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unique identifier for an image-based icon.
|
||||
///
|
||||
/// Used to distinguish cached images and textures by either a file path
|
||||
/// or a Windows window handle.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum ImageIconId {
|
||||
/// Identifier based on a file system path.
|
||||
Path(Arc<Path>),
|
||||
/// Windows HWND handle.
|
||||
Hwnd(isize),
|
||||
}
|
||||
|
||||
impl From<&Path> for ImageIconId {
|
||||
#[inline]
|
||||
fn from(value: &Path) -> Self {
|
||||
Self::Path(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<isize> for ImageIconId {
|
||||
#[inline]
|
||||
fn from(value: isize) -> Self {
|
||||
Self::Hwnd(value)
|
||||
}
|
||||
}
|
||||
540
komorebi-bar/src/widgets/network.rs
Normal file
540
komorebi-bar/src/widgets/network.rs
Normal file
@@ -0,0 +1,540 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::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 num_derive::FromPrimitive;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Networks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct NetworkConfig {
|
||||
/// Enable the Network widget
|
||||
pub enable: bool,
|
||||
/// Show total received and transmitted activity
|
||||
#[serde(alias = "show_total_data_transmitted")]
|
||||
pub show_total_activity: bool,
|
||||
/// Show received and transmitted activity
|
||||
#[serde(alias = "show_network_activity")]
|
||||
pub show_activity: bool,
|
||||
/// Show default interface
|
||||
pub show_default_interface: Option<bool>,
|
||||
/// Characters to reserve for received and transmitted activity
|
||||
#[serde(alias = "network_activity_fill_characters")]
|
||||
pub activity_left_padding: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))
|
||||
pub auto_select: Option<NetworkSelectConfig>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct NetworkSelectConfig {
|
||||
/// Select the total received data when it's over this value
|
||||
pub total_received_over: Option<u64>,
|
||||
/// Select the total transmitted data when it's over this value
|
||||
pub total_transmitted_over: Option<u64>,
|
||||
/// Select the received data when it's over this value
|
||||
pub received_over: Option<u64>,
|
||||
/// Select the transmitted data when it's over this value
|
||||
pub transmitted_over: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<NetworkConfig> for Network {
|
||||
fn from(value: NetworkConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
show_total_activity: value.show_total_activity,
|
||||
show_activity: value.show_activity,
|
||||
show_default_interface: value.show_default_interface.unwrap_or(true),
|
||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
||||
default_interface: String::new(),
|
||||
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))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Network {
|
||||
pub enable: bool,
|
||||
pub show_total_activity: bool,
|
||||
pub show_activity: bool,
|
||||
pub show_default_interface: bool,
|
||||
networks_network_activity: Networks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select: Option<NetworkSelectConfig>,
|
||||
default_interface: String,
|
||||
last_state_total_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
last_updated_network_activity: 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 network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let mut activity = self.last_state_activity.clone();
|
||||
let mut total_activity = self.last_state_total_activity.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
activity.clear();
|
||||
total_activity.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
|
||||
self.networks_network_activity.refresh(true);
|
||||
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if self.show_activity {
|
||||
let received = Self::to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval,
|
||||
);
|
||||
let transmitted = Self::to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval,
|
||||
);
|
||||
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
ReadingValue::from(received),
|
||||
ReadingValue::from(transmitted),
|
||||
));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
fn reading_to_labels(
|
||||
&self,
|
||||
select_received: bool,
|
||||
select_transmitted: bool,
|
||||
ctx: &Context,
|
||||
reading: &NetworkReading,
|
||||
config: RenderConfig,
|
||||
) -> (Label, Label) {
|
||||
let (text_down, text_up) = match self.label_prefix {
|
||||
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"{: >width$}/s ",
|
||||
reading.received.pretty,
|
||||
width = self.activity_left_padding
|
||||
),
|
||||
format!(
|
||||
"{: >width$}/s",
|
||||
reading.transmitted.pretty,
|
||||
width = self.activity_left_padding
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("{} ", reading.received.pretty),
|
||||
reading.transmitted.pretty.clone(),
|
||||
),
|
||||
},
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"DOWN: {: >width$}/s ",
|
||||
reading.received.pretty,
|
||||
width = self.activity_left_padding
|
||||
),
|
||||
format!(
|
||||
"UP: {: >width$}/s",
|
||||
reading.transmitted.pretty,
|
||||
width = self.activity_left_padding
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("\u{2211}DOWN: {}/s ", reading.received.pretty),
|
||||
format!("\u{2211}UP: {}/s", reading.transmitted.pretty),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
let auto_text_color_received = config.auto_select_text.filter(|_| select_received);
|
||||
let auto_text_color_transmitted = config.auto_select_text.filter(|_| select_transmitted);
|
||||
|
||||
// icon
|
||||
let mut layout_job_down = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
if select_received {
|
||||
egui_phosphor::regular::ARROW_FAT_LINES_DOWN.to_string()
|
||||
} else {
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
|
||||
}
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color_received.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job_down.append(
|
||||
&text_down,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color_received.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// icon
|
||||
let mut layout_job_up = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
if select_transmitted {
|
||||
egui_phosphor::regular::ARROW_FAT_LINES_UP.to_string()
|
||||
} else {
|
||||
egui_phosphor::regular::ARROW_FAT_UP.to_string()
|
||||
}
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color_transmitted.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job_up.append(
|
||||
&text_up,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color_transmitted.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
(
|
||||
Label::new(layout_job_down).selectable(false),
|
||||
Label::new(layout_job_up).selectable(false),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> (u64, String) {
|
||||
let input = input_in_bytes as f32 / timespan_in_s as f32;
|
||||
let mut magnitude = input.log(1024f32) as u32;
|
||||
|
||||
// let the base unit be KiB
|
||||
if magnitude < 1 {
|
||||
magnitude = 1;
|
||||
}
|
||||
|
||||
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
|
||||
let result = input / ((1u64) << (magnitude * 10)) as f32;
|
||||
|
||||
(
|
||||
input as u64,
|
||||
match base {
|
||||
Some(DataUnit::B) => format!("{result:.1} B"),
|
||||
Some(unit) => format!("{result:.1} {unit}iB"),
|
||||
None => String::from("Unknown data unit"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn show_frame<R>(
|
||||
&self,
|
||||
selected: bool,
|
||||
auto_focus_fill: Option<Color32>,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) {
|
||||
if SelectableFrame::new_auto(selected, auto_focus_fill)
|
||||
.show(ui, add_contents)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
||||
eprintln!("{}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Network {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
|
||||
|
||||
// widget spacing: make sure to use the same config to call the apply_on_widget function
|
||||
let mut render_config = config.clone();
|
||||
|
||||
if self.show_total_activity || self.show_activity {
|
||||
let (activity, total_activity) = self.network_activity();
|
||||
|
||||
if self.show_total_activity {
|
||||
for reading in &total_activity {
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
let select_received = self.auto_select.is_some_and(|f| {
|
||||
f.total_received_over
|
||||
.is_some_and(|o| reading.received.value > o)
|
||||
});
|
||||
let select_transmitted = self.auto_select.is_some_and(|f| {
|
||||
f.total_transmitted_over
|
||||
.is_some_and(|o| reading.transmitted.value > o)
|
||||
});
|
||||
|
||||
let labels = self.reading_to_labels(
|
||||
select_received,
|
||||
select_transmitted,
|
||||
ctx,
|
||||
reading,
|
||||
config.clone(),
|
||||
);
|
||||
|
||||
if is_reversed {
|
||||
self.show_frame(
|
||||
select_transmitted,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.1),
|
||||
);
|
||||
self.show_frame(
|
||||
select_received,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.0),
|
||||
);
|
||||
} else {
|
||||
self.show_frame(
|
||||
select_received,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.0),
|
||||
);
|
||||
self.show_frame(
|
||||
select_transmitted,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.1),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_activity {
|
||||
for reading in &activity {
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
let select_received = self.auto_select.is_some_and(|f| {
|
||||
f.received_over.is_some_and(|o| reading.received.value > o)
|
||||
});
|
||||
let select_transmitted = self.auto_select.is_some_and(|f| {
|
||||
f.transmitted_over
|
||||
.is_some_and(|o| reading.transmitted.value > o)
|
||||
});
|
||||
|
||||
let labels = self.reading_to_labels(
|
||||
select_received,
|
||||
select_transmitted,
|
||||
ctx,
|
||||
reading,
|
||||
config.clone(),
|
||||
);
|
||||
|
||||
if is_reversed {
|
||||
self.show_frame(
|
||||
select_transmitted,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.1),
|
||||
);
|
||||
self.show_frame(
|
||||
select_received,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.0),
|
||||
);
|
||||
} else {
|
||||
self.show_frame(
|
||||
select_received,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.0),
|
||||
);
|
||||
self.show_frame(
|
||||
select_transmitted,
|
||||
config.auto_select_fill,
|
||||
ui,
|
||||
|ui| ui.add(labels.1),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_default_interface {
|
||||
self.default_interface();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::WIFI_HIGH.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
self.default_interface.insert_str(0, "NET: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&self.default_interface,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
self.show_frame(false, None, ui, |ui| {
|
||||
ui.add(Label::new(layout_job).selectable(false))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// widget spacing: pass on the config that was use for calling the apply_on_widget function
|
||||
*config = render_config.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum NetworkReadingFormat {
|
||||
Speed = 0,
|
||||
Total = 1,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ReadingValue {
|
||||
value: u64,
|
||||
pretty: String,
|
||||
}
|
||||
|
||||
impl From<(u64, String)> for ReadingValue {
|
||||
fn from(value: (u64, String)) -> Self {
|
||||
Self {
|
||||
value: value.0,
|
||||
pretty: value.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NetworkReading {
|
||||
format: NetworkReadingFormat,
|
||||
received: ReadingValue,
|
||||
transmitted: ReadingValue,
|
||||
}
|
||||
|
||||
impl NetworkReading {
|
||||
fn new(
|
||||
format: NetworkReadingFormat,
|
||||
received: ReadingValue,
|
||||
transmitted: ReadingValue,
|
||||
) -> Self {
|
||||
Self {
|
||||
format,
|
||||
received,
|
||||
transmitted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPrimitive)]
|
||||
enum DataUnit {
|
||||
B = 0,
|
||||
K = 1,
|
||||
M = 2,
|
||||
G = 3,
|
||||
T = 4,
|
||||
P = 5,
|
||||
E = 6,
|
||||
Z = 7,
|
||||
Y = 8,
|
||||
}
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
@@ -24,6 +25,10 @@ pub struct StorageConfig {
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
/// Hide when the current percentage is under this value [[1-100]]
|
||||
pub auto_hide_under: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<StorageConfig> for Storage {
|
||||
@@ -33,21 +38,30 @@ 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),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StorageDisk {
|
||||
label: String,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
pub struct Storage {
|
||||
pub enable: bool,
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select_over: Option<u8>,
|
||||
auto_hide_under: Option<u8>,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
fn output(&mut self) -> Vec<String> {
|
||||
fn output(&mut self) -> Vec<StorageDisk> {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.disks.refresh(true);
|
||||
@@ -61,17 +75,26 @@ impl Storage {
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
let used = total - available;
|
||||
let percentage = ((used * 100) / total) as u8;
|
||||
|
||||
disks.push(match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
})
|
||||
let hide = self.auto_hide_under.is_some_and(|u| percentage <= u);
|
||||
|
||||
if !hide {
|
||||
let selected = self.auto_select_over.is_some_and(|o| percentage >= o);
|
||||
|
||||
disks.push(StorageDisk {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), percentage)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
|
||||
},
|
||||
selected,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
disks.sort();
|
||||
disks.reverse();
|
||||
disks.sort_by(|a, b| a.label.cmp(&b.label));
|
||||
|
||||
disks
|
||||
}
|
||||
@@ -80,7 +103,16 @@ impl Storage {
|
||||
impl BarWidget for Storage {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
for output in self.output() {
|
||||
let mut output = self.output();
|
||||
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
|
||||
|
||||
if is_reversed {
|
||||
output.reverse();
|
||||
}
|
||||
|
||||
for output in output {
|
||||
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
@@ -89,23 +121,25 @@ impl BarWidget for Storage {
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
&output.label,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let auto_focus_fill = config.auto_select_fill;
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
@@ -113,7 +147,7 @@ impl BarWidget for Storage {
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.split(' ').collect::<Vec<&str>>()[0],
|
||||
output.label.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
@@ -2,7 +2,10 @@ use crate::bar::Alignment;
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
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;
|
||||
@@ -14,8 +17,58 @@ use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::epaint::StrokeKind;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
lazy_static! {
|
||||
static ref TIME_RANGES: Vec<(&'static str, NaiveTime)> = {
|
||||
vec![
|
||||
(
|
||||
egui_phosphor::regular::MOON,
|
||||
NaiveTime::from_hms_opt(0, 0, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::ALARM,
|
||||
NaiveTime::from_hms_opt(6, 0, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::BREAD,
|
||||
NaiveTime::from_hms_opt(6, 1, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::BARBELL,
|
||||
NaiveTime::from_hms_opt(6, 30, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::COFFEE,
|
||||
NaiveTime::from_hms_opt(8, 0, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::CLOCK,
|
||||
NaiveTime::from_hms_opt(8, 30, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::HAMBURGER,
|
||||
NaiveTime::from_hms_opt(12, 0, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::CLOCK_AFTERNOON,
|
||||
NaiveTime::from_hms_opt(12, 30, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::FORK_KNIFE,
|
||||
NaiveTime::from_hms_opt(18, 0, 0).expect("invalid"),
|
||||
),
|
||||
(
|
||||
egui_phosphor::regular::MOON_STARS,
|
||||
NaiveTime::from_hms_opt(18, 30, 0).expect("invalid"),
|
||||
),
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
@@ -26,14 +79,40 @@ pub struct TimeConfig {
|
||||
pub format: TimeFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// TimeZone (https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html)
|
||||
///
|
||||
/// Use a custom format to display additional information, i.e.:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "Time": {
|
||||
/// "enable": true,
|
||||
/// "format": { "Custom": "%T %Z (Tokyo)" },
|
||||
/// "timezone": "Asia/Tokyo"
|
||||
/// }
|
||||
///}
|
||||
/// ```
|
||||
pub timezone: Option<String>,
|
||||
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00. (default: false)
|
||||
pub changing_icon: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<TimeConfig> for Time {
|
||||
fn from(value: TimeConfig) -> Self {
|
||||
// using 1 second made the widget look "less accurate" and lagging (especially having multiple with seconds).
|
||||
// This is still better than getting an update every frame
|
||||
let data_refresh_interval = 500;
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
timezone: value.timezone,
|
||||
changing_icon: value.changing_icon.unwrap_or_default(),
|
||||
data_refresh_interval_millis: data_refresh_interval,
|
||||
last_state: TimeOutput::new(),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_millis(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,20 +162,94 @@ impl TimeFormat {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TimeOutput {
|
||||
label: String,
|
||||
icon: String,
|
||||
}
|
||||
|
||||
impl TimeOutput {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
label: String::new(),
|
||||
icon: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Time {
|
||||
pub enable: bool,
|
||||
pub format: TimeFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
timezone: Option<String>,
|
||||
changing_icon: bool,
|
||||
data_refresh_interval_millis: u64,
|
||||
last_state: TimeOutput,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Time {
|
||||
fn output(&mut self) -> String {
|
||||
chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string()
|
||||
fn output(&mut self) -> TimeOutput {
|
||||
let mut output = self.last_state.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated)
|
||||
> Duration::from_millis(self.data_refresh_interval_millis)
|
||||
{
|
||||
let (formatted, current_time) = match &self.timezone {
|
||||
Some(timezone) => match timezone.parse::<Tz>() {
|
||||
Ok(tz) => {
|
||||
let dt = Local::now().with_timezone(&tz);
|
||||
(
|
||||
dt.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
Some(dt.time()),
|
||||
)
|
||||
}
|
||||
Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
|
||||
},
|
||||
None => {
|
||||
let dt = Local::now();
|
||||
(
|
||||
dt.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
Some(dt.time()),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if current_time.is_none() {
|
||||
return TimeOutput {
|
||||
label: formatted,
|
||||
icon: egui_phosphor::regular::WARNING_CIRCLE.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let current_range = match &self.changing_icon {
|
||||
true => TIME_RANGES
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|&(_, start)| current_time.unwrap() > *start)
|
||||
.cloned(),
|
||||
false => None,
|
||||
}
|
||||
.unwrap_or((egui_phosphor::regular::CLOCK, NaiveTime::default()));
|
||||
|
||||
output = TimeOutput {
|
||||
label: formatted,
|
||||
icon: current_range.0.to_string(),
|
||||
};
|
||||
|
||||
self.last_state.clone_from(&output);
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn paint_binary_circle(
|
||||
@@ -279,15 +432,13 @@ impl BarWidget for Time {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let use_binary_circle = output.starts_with('c');
|
||||
let use_binary_rectangle = output.starts_with('r');
|
||||
if !output.label.is_empty() {
|
||||
let use_binary_circle = output.label.starts_with('c');
|
||||
let use_binary_rectangle = output.label.starts_with('r');
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CLOCK.to_string()
|
||||
}
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => output.icon,
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
@@ -296,12 +447,12 @@ impl BarWidget for Time {
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.insert_str(0, "TIME: ");
|
||||
output.label.insert_str(0, "TIME: ");
|
||||
}
|
||||
|
||||
if !use_binary_circle && !use_binary_rectangle {
|
||||
layout_job.append(
|
||||
&output,
|
||||
&output.label,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
@@ -324,9 +475,9 @@ impl BarWidget for Time {
|
||||
|
||||
if use_binary_circle || use_binary_rectangle {
|
||||
let ordered_output = if is_reversed {
|
||||
output.chars().rev().collect()
|
||||
output.label.chars().rev().collect()
|
||||
} else {
|
||||
output
|
||||
output.label
|
||||
};
|
||||
|
||||
for (section_index, section) in
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
@@ -1,26 +1,28 @@
|
||||
use crate::battery::Battery;
|
||||
use crate::battery::BatteryConfig;
|
||||
use crate::cpu::Cpu;
|
||||
use crate::cpu::CpuConfig;
|
||||
use crate::date::Date;
|
||||
use crate::date::DateConfig;
|
||||
use crate::keyboard::Keyboard;
|
||||
use crate::keyboard::KeyboardConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiConfig;
|
||||
use crate::media::Media;
|
||||
use crate::media::MediaConfig;
|
||||
use crate::memory::Memory;
|
||||
use crate::memory::MemoryConfig;
|
||||
use crate::network::Network;
|
||||
use crate::network::NetworkConfig;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::storage::Storage;
|
||||
use crate::storage::StorageConfig;
|
||||
use crate::time::Time;
|
||||
use crate::time::TimeConfig;
|
||||
use crate::update::Update;
|
||||
use crate::update::UpdateConfig;
|
||||
use crate::widgets::applications::Applications;
|
||||
use crate::widgets::applications::ApplicationsConfig;
|
||||
use crate::widgets::battery::Battery;
|
||||
use crate::widgets::battery::BatteryConfig;
|
||||
use crate::widgets::cpu::Cpu;
|
||||
use crate::widgets::cpu::CpuConfig;
|
||||
use crate::widgets::date::Date;
|
||||
use crate::widgets::date::DateConfig;
|
||||
use crate::widgets::keyboard::Keyboard;
|
||||
use crate::widgets::keyboard::KeyboardConfig;
|
||||
use crate::widgets::komorebi::Komorebi;
|
||||
use crate::widgets::komorebi::KomorebiConfig;
|
||||
use crate::widgets::media::Media;
|
||||
use crate::widgets::media::MediaConfig;
|
||||
use crate::widgets::memory::Memory;
|
||||
use crate::widgets::memory::MemoryConfig;
|
||||
use crate::widgets::network::Network;
|
||||
use crate::widgets::network::NetworkConfig;
|
||||
use crate::widgets::storage::Storage;
|
||||
use crate::widgets::storage::StorageConfig;
|
||||
use crate::widgets::time::Time;
|
||||
use crate::widgets::time::TimeConfig;
|
||||
use crate::widgets::update::Update;
|
||||
use crate::widgets::update::UpdateConfig;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Ui;
|
||||
use serde::Deserialize;
|
||||
@@ -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,
|
||||
@@ -72,7 +77,7 @@ impl WidgetConfig {
|
||||
WidgetConfig::Komorebi(config) => {
|
||||
config.workspaces.as_ref().is_some_and(|w| w.enable)
|
||||
|| config.layout.as_ref().is_some_and(|w| w.enable)
|
||||
|| config.focused_window.as_ref().is_some_and(|w| w.enable)
|
||||
|| config.focused_container.as_ref().is_some_and(|w| w.enable)
|
||||
|| config
|
||||
.configuration_switcher
|
||||
.as_ref()
|
||||
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.35"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
# 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"]
|
||||
|
||||
@@ -5,8 +5,6 @@ pub use komorebi::animation::prefix::AnimationPrefix;
|
||||
pub use komorebi::animation::PerAnimationPrefixConfig;
|
||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||
pub use komorebi::border_manager::BorderInfo;
|
||||
pub use komorebi::colour::Colour;
|
||||
pub use komorebi::colour::Rgb;
|
||||
pub use komorebi::config_generation::ApplicationConfiguration;
|
||||
pub use komorebi::config_generation::IdWithIdentifier;
|
||||
pub use komorebi::config_generation::IdWithIdentifierAndComment;
|
||||
@@ -14,7 +12,7 @@ pub use komorebi::config_generation::MatchingRule;
|
||||
pub use komorebi::config_generation::MatchingStrategy;
|
||||
pub use komorebi::container::Container;
|
||||
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
||||
pub use komorebi::core::resolve_home_path;
|
||||
pub use komorebi::core::replace_env_in_path;
|
||||
pub use komorebi::core::AnimationStyle;
|
||||
pub use komorebi::core::ApplicationIdentifier;
|
||||
pub use komorebi::core::Arrangement;
|
||||
@@ -29,6 +27,7 @@ pub use komorebi::core::CustomLayout;
|
||||
pub use komorebi::core::CycleDirection;
|
||||
pub use komorebi::core::DefaultLayout;
|
||||
pub use komorebi::core::Direction;
|
||||
pub use komorebi::core::FloatingLayerBehaviour;
|
||||
pub use komorebi::core::FocusFollowsMouseImplementation;
|
||||
pub use komorebi::core::HidingBehaviour;
|
||||
pub use komorebi::core::Layout;
|
||||
@@ -46,14 +45,17 @@ pub use komorebi::core::WindowKind;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
pub use komorebi::monitor_reconciliator::MonitorNotification;
|
||||
pub use komorebi::ring::Ring;
|
||||
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;
|
||||
@@ -61,12 +63,14 @@ pub use komorebi::MonitorConfig;
|
||||
pub use komorebi::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::PredefinedAspectRatio;
|
||||
pub use komorebi::Rgb;
|
||||
pub use komorebi::RuleDebug;
|
||||
pub use komorebi::StackbarConfig;
|
||||
pub use komorebi::State;
|
||||
pub use komorebi::StaticConfig;
|
||||
pub use komorebi::SubscribeOptions;
|
||||
pub use komorebi::TabsConfig;
|
||||
pub use komorebi::VirtualDesktopNotification;
|
||||
pub use komorebi::WindowContainerBehaviour;
|
||||
pub use komorebi::WindowsApi;
|
||||
pub use komorebi::WorkspaceConfig;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.35"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
random_word = { version = "0.4", features = ["en"] }
|
||||
random_word = { version = "0.5", features = ["en"] }
|
||||
serde_json = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
@@ -41,7 +41,9 @@ struct BorderColours {
|
||||
single: Color32,
|
||||
stack: Color32,
|
||||
monocle: Color32,
|
||||
floating: Color32,
|
||||
unfocused: Color32,
|
||||
unfocused_locked: Color32,
|
||||
}
|
||||
|
||||
struct BorderConfig {
|
||||
@@ -101,7 +103,7 @@ impl From<&komorebi_client::Workspace> for WorkspaceConfig {
|
||||
let name = value
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| random_word::gen(random_word::Lang::En).to_string());
|
||||
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
|
||||
|
||||
Self {
|
||||
layout,
|
||||
@@ -154,7 +156,9 @@ impl KomorebiGui {
|
||||
single: colour32(global_state.border_colours.single),
|
||||
stack: colour32(global_state.border_colours.stack),
|
||||
monocle: colour32(global_state.border_colours.monocle),
|
||||
floating: colour32(global_state.border_colours.floating),
|
||||
unfocused: colour32(global_state.border_colours.unfocused),
|
||||
unfocused_locked: colour32(global_state.border_colours.unfocused_locked),
|
||||
};
|
||||
|
||||
let border_config = BorderConfig {
|
||||
@@ -377,6 +381,22 @@ impl eframe::App for KomorebiGui {
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Floating", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.border_config.border_colours.floating,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(&SocketMessage::BorderColour(
|
||||
WindowKind::Floating,
|
||||
self.border_config.border_colours.floating.r() as u32,
|
||||
self.border_config.border_colours.floating.g() as u32,
|
||||
self.border_config.border_colours.floating.b() as u32,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Unfocused", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
@@ -391,6 +411,22 @@ impl eframe::App for KomorebiGui {
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Unfocused Locked", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.border_config.border_colours.unfocused_locked,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(&SocketMessage::BorderColour(
|
||||
WindowKind::UnfocusedLocked,
|
||||
self.border_config.border_colours.unfocused_locked.r() as u32,
|
||||
self.border_config.border_colours.unfocused_locked.g() as u32,
|
||||
self.border_config.border_colours.unfocused_locked.b() as u32,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
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.9" }
|
||||
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.9" }
|
||||
|
||||
eframe = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
106
komorebi-shortcuts/src/main.rs
Normal file
106
komorebi-shortcuts/src/main.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use std::path::PathBuf;
|
||||
use whkd_core::HotkeyBinding;
|
||||
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)
|
||||
.background_color(ctx.style().visuals.faint_bg_color),
|
||||
);
|
||||
ui.end_row();
|
||||
ui.end_row();
|
||||
|
||||
for binding in &whkdrc.bindings {
|
||||
if is_komorebic_binding(binding) {
|
||||
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();
|
||||
}
|
||||
|
||||
fn is_komorebic_binding(binding: &HotkeyBinding) -> bool {
|
||||
binding.command.starts_with("komorebic")
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.35"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -8,7 +8,13 @@ base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev
|
||||
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"] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_variant = "0.1"
|
||||
strum = { workspace = true }
|
||||
hex_color = { version = "3", features = ["serde"] }
|
||||
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["dep:schemars"]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use hex_color::HexColor;
|
||||
use komorebi_themes::Color32;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::gen::SchemaGenerator;
|
||||
#[cfg(feature = "schemars")]
|
||||
@@ -9,6 +8,7 @@ use schemars::schema::Schema;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::SchemaObject;
|
||||
|
||||
use crate::Color32;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -57,7 +57,7 @@ impl From<Colour> for Color32 {
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Hex(HexColor);
|
||||
pub struct Hex(pub HexColor);
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for Hex {
|
||||
77
komorebi-themes/src/generator.rs
Normal file
77
komorebi-themes/src/generator.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::colour::Colour;
|
||||
use crate::colour::Hex;
|
||||
use crate::Base16ColourPalette;
|
||||
use hex_color::HexColor;
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum ThemeVariant {
|
||||
#[default]
|
||||
Dark,
|
||||
Light,
|
||||
}
|
||||
|
||||
impl Display for ThemeVariant {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ThemeVariant::Dark => write!(f, "dark"),
|
||||
ThemeVariant::Light => write!(f, "light"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ThemeVariant> for flavours::operations::generate::Mode {
|
||||
fn from(value: ThemeVariant) -> Self {
|
||||
match value {
|
||||
ThemeVariant::Dark => Self::Dark,
|
||||
ThemeVariant::Light => Self::Light,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_base16_palette(
|
||||
image_path: &Path,
|
||||
variant: ThemeVariant,
|
||||
) -> Result<Base16ColourPalette, hex_color::ParseHexColorError> {
|
||||
Base16ColourPalette::try_from(
|
||||
&flavours::operations::generate::generate(image_path, variant.into(), false)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
impl TryFrom<&VecDeque<String>> for Base16ColourPalette {
|
||||
type Error = hex_color::ParseHexColorError;
|
||||
|
||||
fn try_from(value: &VecDeque<String>) -> Result<Self, Self::Error> {
|
||||
let fixed = value.iter().map(|s| format!("#{s}")).collect::<Vec<_>>();
|
||||
if fixed.len() != 16 {
|
||||
return Err(hex_color::ParseHexColorError::Empty);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
base_00: Colour::Hex(Hex(HexColor::parse(&fixed[0])?)),
|
||||
base_01: Colour::Hex(Hex(HexColor::parse(&fixed[1])?)),
|
||||
base_02: Colour::Hex(Hex(HexColor::parse(&fixed[2])?)),
|
||||
base_03: Colour::Hex(Hex(HexColor::parse(&fixed[3])?)),
|
||||
base_04: Colour::Hex(Hex(HexColor::parse(&fixed[4])?)),
|
||||
base_05: Colour::Hex(Hex(HexColor::parse(&fixed[5])?)),
|
||||
base_06: Colour::Hex(Hex(HexColor::parse(&fixed[6])?)),
|
||||
base_07: Colour::Hex(Hex(HexColor::parse(&fixed[7])?)),
|
||||
base_08: Colour::Hex(Hex(HexColor::parse(&fixed[8])?)),
|
||||
base_09: Colour::Hex(Hex(HexColor::parse(&fixed[9])?)),
|
||||
base_0a: Colour::Hex(Hex(HexColor::parse(&fixed[10])?)),
|
||||
base_0b: Colour::Hex(Hex(HexColor::parse(&fixed[11])?)),
|
||||
base_0c: Colour::Hex(Hex(HexColor::parse(&fixed[12])?)),
|
||||
base_0d: Colour::Hex(Hex(HexColor::parse(&fixed[13])?)),
|
||||
base_0e: Colour::Hex(Hex(HexColor::parse(&fixed[14])?)),
|
||||
base_0f: Colour::Hex(Hex(HexColor::parse(&fixed[15])?)),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,32 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub mod colour;
|
||||
mod generator;
|
||||
|
||||
pub use generator::generate_base16_palette;
|
||||
pub use generator::ThemeVariant;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::colour::Colour;
|
||||
pub use base16_egui_themes::Base16;
|
||||
pub use catppuccin_egui;
|
||||
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 serde_variant::to_variant_name;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Theme {
|
||||
/// A theme from catppuccin-egui
|
||||
@@ -25,6 +39,140 @@ pub enum Theme {
|
||||
name: Base16,
|
||||
accent: Option<Base16Value>,
|
||||
},
|
||||
/// A custom base16 palette
|
||||
Custom {
|
||||
palette: Box<Base16ColourPalette>,
|
||||
accent: Option<Base16Value>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Base16ColourPalette {
|
||||
pub base_00: Colour,
|
||||
pub base_01: Colour,
|
||||
pub base_02: Colour,
|
||||
pub base_03: Colour,
|
||||
pub base_04: Colour,
|
||||
pub base_05: Colour,
|
||||
pub base_06: Colour,
|
||||
pub base_07: Colour,
|
||||
pub base_08: Colour,
|
||||
pub base_09: Colour,
|
||||
pub base_0a: Colour,
|
||||
pub base_0b: Colour,
|
||||
pub base_0c: Colour,
|
||||
pub base_0d: Colour,
|
||||
pub base_0e: Colour,
|
||||
pub base_0f: Colour,
|
||||
}
|
||||
|
||||
impl Base16ColourPalette {
|
||||
pub fn background(self) -> Color32 {
|
||||
self.base_01.into()
|
||||
}
|
||||
pub fn style(self) -> Style {
|
||||
let original = Style::default();
|
||||
Style {
|
||||
visuals: Visuals {
|
||||
widgets: Widgets {
|
||||
noninteractive: WidgetVisuals {
|
||||
bg_fill: self.base_01.into(),
|
||||
weak_bg_fill: self.base_01.into(),
|
||||
bg_stroke: Stroke {
|
||||
color: self.base_02.into(),
|
||||
..original.visuals.widgets.noninteractive.bg_stroke
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
color: self.base_05.into(),
|
||||
..original.visuals.widgets.noninteractive.fg_stroke
|
||||
},
|
||||
..original.visuals.widgets.noninteractive
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
bg_fill: self.base_02.into(),
|
||||
weak_bg_fill: self.base_02.into(),
|
||||
bg_stroke: Stroke {
|
||||
color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
|
||||
..original.visuals.widgets.inactive.bg_stroke
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
color: self.base_05.into(),
|
||||
..original.visuals.widgets.inactive.fg_stroke
|
||||
},
|
||||
..original.visuals.widgets.inactive
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
bg_fill: self.base_02.into(),
|
||||
weak_bg_fill: self.base_02.into(),
|
||||
bg_stroke: Stroke {
|
||||
color: self.base_03.into(),
|
||||
..original.visuals.widgets.hovered.bg_stroke
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
color: self.base_06.into(),
|
||||
..original.visuals.widgets.hovered.fg_stroke
|
||||
},
|
||||
..original.visuals.widgets.hovered
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
bg_fill: self.base_02.into(),
|
||||
weak_bg_fill: self.base_02.into(),
|
||||
bg_stroke: Stroke {
|
||||
color: self.base_03.into(),
|
||||
..original.visuals.widgets.hovered.bg_stroke
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
color: self.base_06.into(),
|
||||
..original.visuals.widgets.hovered.fg_stroke
|
||||
},
|
||||
..original.visuals.widgets.active
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
bg_fill: self.base_01.into(),
|
||||
weak_bg_fill: self.base_01.into(),
|
||||
bg_stroke: Stroke {
|
||||
color: self.base_02.into(),
|
||||
..original.visuals.widgets.open.bg_stroke
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
color: self.base_06.into(),
|
||||
..original.visuals.widgets.open.fg_stroke
|
||||
},
|
||||
..original.visuals.widgets.open
|
||||
},
|
||||
},
|
||||
selection: Selection {
|
||||
bg_fill: self.base_02.into(),
|
||||
stroke: Stroke {
|
||||
color: self.base_06.into(),
|
||||
..original.visuals.selection.stroke
|
||||
},
|
||||
},
|
||||
hyperlink_color: self.base_08.into(),
|
||||
faint_bg_color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
|
||||
extreme_bg_color: self.base_00.into(),
|
||||
code_bg_color: self.base_02.into(),
|
||||
warn_fg_color: self.base_0c.into(),
|
||||
error_fg_color: self.base_0b.into(),
|
||||
window_shadow: Shadow {
|
||||
color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
|
||||
..original.visuals.window_shadow
|
||||
},
|
||||
window_fill: self.base_01.into(),
|
||||
window_stroke: Stroke {
|
||||
color: self.base_02.into(),
|
||||
..original.visuals.window_stroke
|
||||
},
|
||||
panel_fill: self.base_01.into(),
|
||||
popup_shadow: Shadow {
|
||||
color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
|
||||
..original.visuals.popup_shadow
|
||||
},
|
||||
..original.visuals
|
||||
},
|
||||
..original
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
@@ -45,6 +193,7 @@ impl Theme {
|
||||
.to_string()
|
||||
})
|
||||
.collect(),
|
||||
Theme::Custom { .. } => vec!["Custom".to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,25 +219,50 @@ pub enum Base16Value {
|
||||
Base0F,
|
||||
}
|
||||
|
||||
pub enum Base16Wrapper {
|
||||
Base16(Base16),
|
||||
Custom(Box<Base16ColourPalette>),
|
||||
}
|
||||
|
||||
impl Base16Value {
|
||||
pub fn color32(&self, theme: Base16) -> Color32 {
|
||||
match self {
|
||||
Base16Value::Base00 => theme.base00(),
|
||||
Base16Value::Base01 => theme.base01(),
|
||||
Base16Value::Base02 => theme.base02(),
|
||||
Base16Value::Base03 => theme.base03(),
|
||||
Base16Value::Base04 => theme.base04(),
|
||||
Base16Value::Base05 => theme.base05(),
|
||||
Base16Value::Base06 => theme.base06(),
|
||||
Base16Value::Base07 => theme.base07(),
|
||||
Base16Value::Base08 => theme.base08(),
|
||||
Base16Value::Base09 => theme.base09(),
|
||||
Base16Value::Base0A => theme.base0a(),
|
||||
Base16Value::Base0B => theme.base0b(),
|
||||
Base16Value::Base0C => theme.base0c(),
|
||||
Base16Value::Base0D => theme.base0d(),
|
||||
Base16Value::Base0E => theme.base0e(),
|
||||
Base16Value::Base0F => theme.base0f(),
|
||||
pub fn color32(&self, theme: Base16Wrapper) -> Color32 {
|
||||
match theme {
|
||||
Base16Wrapper::Base16(theme) => match self {
|
||||
Base16Value::Base00 => theme.base00(),
|
||||
Base16Value::Base01 => theme.base01(),
|
||||
Base16Value::Base02 => theme.base02(),
|
||||
Base16Value::Base03 => theme.base03(),
|
||||
Base16Value::Base04 => theme.base04(),
|
||||
Base16Value::Base05 => theme.base05(),
|
||||
Base16Value::Base06 => theme.base06(),
|
||||
Base16Value::Base07 => theme.base07(),
|
||||
Base16Value::Base08 => theme.base08(),
|
||||
Base16Value::Base09 => theme.base09(),
|
||||
Base16Value::Base0A => theme.base0a(),
|
||||
Base16Value::Base0B => theme.base0b(),
|
||||
Base16Value::Base0C => theme.base0c(),
|
||||
Base16Value::Base0D => theme.base0d(),
|
||||
Base16Value::Base0E => theme.base0e(),
|
||||
Base16Value::Base0F => theme.base0f(),
|
||||
},
|
||||
Base16Wrapper::Custom(colours) => match self {
|
||||
Base16Value::Base00 => colours.base_00.into(),
|
||||
Base16Value::Base01 => colours.base_01.into(),
|
||||
Base16Value::Base02 => colours.base_02.into(),
|
||||
Base16Value::Base03 => colours.base_03.into(),
|
||||
Base16Value::Base04 => colours.base_04.into(),
|
||||
Base16Value::Base05 => colours.base_05.into(),
|
||||
Base16Value::Base06 => colours.base_06.into(),
|
||||
Base16Value::Base07 => colours.base_07.into(),
|
||||
Base16Value::Base08 => colours.base_08.into(),
|
||||
Base16Value::Base09 => colours.base_09.into(),
|
||||
Base16Value::Base0A => colours.base_0a.into(),
|
||||
Base16Value::Base0B => colours.base_0b.into(),
|
||||
Base16Value::Base0C => colours.base_0c.into(),
|
||||
Base16Value::Base0D => colours.base_0d.into(),
|
||||
Base16Value::Base0E => colours.base_0e.into(),
|
||||
Base16Value::Base0F => colours.base_0f.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.35"
|
||||
version = "0.1.38"
|
||||
description = "A tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
@@ -17,17 +17,16 @@ crossbeam-channel = { workspace = true }
|
||||
crossbeam-utils = { workspace = true }
|
||||
ctrlc = { version = "3", features = ["termination"] }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
getset = "0.1"
|
||||
hex_color = { version = "3", features = ["serde"] }
|
||||
hotwatch = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
miow = "0.6"
|
||||
nanoid = "0.4"
|
||||
net2 = "0.2"
|
||||
os_info = "3.10"
|
||||
parking_lot = "0.12"
|
||||
parking_lot = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
powershell_script = "1.0"
|
||||
regex = "1"
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
@@ -49,6 +48,7 @@ windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.55"
|
||||
serde_with = { version = "3.12", features = ["schemars_0_8"] }
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,11 @@ use crate::core::Rect;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Foundation::FALSE;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
@@ -51,18 +49,22 @@ 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::IDC_ARROW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows_core::BOOL;
|
||||
use windows_core::PCWSTR;
|
||||
@@ -114,7 +116,7 @@ pub struct Border {
|
||||
pub hwnd: isize,
|
||||
pub id: String,
|
||||
pub monitor_idx: Option<usize>,
|
||||
pub render_target: OnceLock<RenderTarget>,
|
||||
pub render_target: Option<RenderTarget>,
|
||||
pub tracking_hwnd: isize,
|
||||
pub window_rect: Rect,
|
||||
pub window_kind: WindowKind,
|
||||
@@ -132,7 +134,7 @@ impl From<isize> for Border {
|
||||
hwnd: value,
|
||||
id: String::new(),
|
||||
monitor_idx: None,
|
||||
render_target: OnceLock::new(),
|
||||
render_target: None,
|
||||
tracking_hwnd: 0,
|
||||
window_rect: Rect::default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
@@ -180,7 +182,7 @@ impl Border {
|
||||
hwnd: 0,
|
||||
id: container_id,
|
||||
monitor_idx: Some(monitor_idx),
|
||||
render_target: OnceLock::new(),
|
||||
render_target: None,
|
||||
tracking_hwnd,
|
||||
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
@@ -239,8 +241,14 @@ impl Border {
|
||||
let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh);
|
||||
}
|
||||
|
||||
border.update_brushes()?;
|
||||
|
||||
Ok(border)
|
||||
}
|
||||
|
||||
pub fn update_brushes(&mut self) -> color_eyre::Result<()> {
|
||||
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
|
||||
hwnd: HWND(windows_api::as_ptr!(border.hwnd)),
|
||||
hwnd: HWND(windows_api::as_ptr!(self.hwnd)),
|
||||
pixelSize: Default::default(),
|
||||
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
|
||||
};
|
||||
@@ -261,13 +269,14 @@ impl Border {
|
||||
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
|
||||
} {
|
||||
Ok(render_target) => unsafe {
|
||||
border.brush_properties = *BRUSH_PROPERTIES.deref();
|
||||
self.brush_properties = *BRUSH_PROPERTIES.deref();
|
||||
for window_kind in [
|
||||
WindowKind::Single,
|
||||
WindowKind::Stack,
|
||||
WindowKind::Monocle,
|
||||
WindowKind::Unfocused,
|
||||
WindowKind::Floating,
|
||||
WindowKind::UnfocusedLocked,
|
||||
] {
|
||||
let color = window_kind_colour(window_kind);
|
||||
let color = D2D1_COLOR_F {
|
||||
@@ -278,24 +287,18 @@ impl Border {
|
||||
};
|
||||
|
||||
if let Ok(brush) =
|
||||
render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties))
|
||||
render_target.CreateSolidColorBrush(&color, Some(&self.brush_properties))
|
||||
{
|
||||
border.brushes.insert(window_kind, brush);
|
||||
self.brushes.insert(window_kind, brush);
|
||||
}
|
||||
}
|
||||
|
||||
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||
|
||||
if border
|
||||
.render_target
|
||||
.set(RenderTarget(render_target.clone()))
|
||||
.is_err()
|
||||
{
|
||||
return Err(anyhow!("could not store border render target"));
|
||||
}
|
||||
self.render_target = Some(RenderTarget(render_target));
|
||||
|
||||
border.rounded_rect = {
|
||||
let radius = 8.0 + border.width as f32 / 2.0;
|
||||
self.rounded_rect = {
|
||||
let radius = 8.0 + self.width as f32 / 2.0;
|
||||
D2D1_ROUNDED_RECT {
|
||||
rect: Default::default(),
|
||||
radiusX: radius,
|
||||
@@ -303,7 +306,7 @@ impl Border {
|
||||
}
|
||||
};
|
||||
|
||||
Ok(border)
|
||||
Ok(())
|
||||
},
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
@@ -336,6 +339,16 @@ impl Border {
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_SETCURSOR => match LoadCursorW(None, IDC_ARROW) {
|
||||
Ok(cursor) => {
|
||||
SetCursor(Some(cursor));
|
||||
LRESULT(0)
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
LRESULT(1)
|
||||
}
|
||||
},
|
||||
WM_CREATE => {
|
||||
let mut border_pointer: *mut Border =
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
@@ -379,8 +392,8 @@ 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.get() {
|
||||
if !rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect) {
|
||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
@@ -462,7 +475,7 @@ impl Border {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if let Some(render_target) = (*border_pointer).render_target.get() {
|
||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
||||
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
||||
|
||||
|
||||
@@ -6,10 +6,8 @@ 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::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::Colour;
|
||||
use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
@@ -18,6 +16,8 @@ use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_themes::colour::Colour;
|
||||
use komorebi_themes::colour::Rgb;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
@@ -48,6 +48,8 @@ lazy_static! {
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245))));
|
||||
pub static ref UNFOCUSED: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(128, 128, 128))));
|
||||
pub static ref UNFOCUSED_LOCKED: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(158, 8, 8))));
|
||||
pub static ref MONOCLE: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(255, 51, 153))));
|
||||
pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(0, 165, 66))));
|
||||
@@ -72,7 +74,10 @@ impl Deref for RenderTarget {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Notification(pub Option<isize>);
|
||||
pub enum Notification {
|
||||
Update(Option<isize>),
|
||||
ForceUpdate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct BorderInfo {
|
||||
@@ -101,16 +106,21 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_notification(hwnd: Option<isize>) {
|
||||
if event_tx().try_send(Notification(hwnd)).is_err() {
|
||||
if event_tx().try_send(Notification::Update(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_force_update() {
|
||||
if event_tx().try_send(Notification::ForceUpdate).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
@@ -126,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![];
|
||||
@@ -149,6 +161,7 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
||||
match focus_kind {
|
||||
WindowKind::Unfocused => UNFOCUSED.load(Ordering::Relaxed),
|
||||
WindowKind::UnfocusedLocked => UNFOCUSED_LOCKED.load(Ordering::Relaxed),
|
||||
WindowKind::Single => FOCUSED.load(Ordering::Relaxed),
|
||||
WindowKind::Stack => STACK.load(Ordering::Relaxed),
|
||||
WindowKind::Monocle => MONOCLE.load(Ordering::Relaxed),
|
||||
@@ -173,7 +186,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
event_tx().send(Notification(None))?;
|
||||
event_tx().send(Notification::Update(None))?;
|
||||
|
||||
let mut previous_snapshot = Ring::default();
|
||||
let mut previous_pending_move_op = None;
|
||||
@@ -200,6 +213,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
[focused_workspace_idx]
|
||||
.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);
|
||||
|
||||
@@ -222,6 +237,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;
|
||||
}
|
||||
|
||||
@@ -229,7 +255,11 @@ 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
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
}
|
||||
} else if c.windows().len() > 1 {
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
@@ -255,67 +285,78 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
}
|
||||
BorderImplementation::Komorebi => {
|
||||
let mut should_process_notification = true;
|
||||
let should_process_notification = match notification {
|
||||
Notification::Update(notification_hwnd) => {
|
||||
let mut should_process_notification = true;
|
||||
|
||||
if monitors == previous_snapshot
|
||||
// handle the window dragging edge case
|
||||
&& pending_move_op == previous_pending_move_op
|
||||
{
|
||||
should_process_notification = false;
|
||||
}
|
||||
if monitors == previous_snapshot
|
||||
// handle the window dragging edge case
|
||||
&& pending_move_op == previous_pending_move_op
|
||||
{
|
||||
should_process_notification = false;
|
||||
}
|
||||
|
||||
// handle the pause edge case
|
||||
if is_paused && !previous_is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the unpause edge case
|
||||
if previous_is_paused && !is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the retile edge case
|
||||
if !should_process_notification && BORDER_STATE.lock().is_empty() {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// when we switch focus to/from a floating window
|
||||
let switch_focus_to_from_floating_window = floating_window_hwnds.iter().any(|fw| {
|
||||
// if we switch focus to a floating window
|
||||
fw == ¬ification.0.unwrap_or_default() ||
|
||||
// if there is any floating window with a `WindowKind::Floating` border
|
||||
// that no longer is the foreground window then we need to update that
|
||||
// border.
|
||||
(fw != &foreground_window
|
||||
&& window_border(*fw)
|
||||
.is_some_and(|b| b.window_kind == WindowKind::Floating))
|
||||
});
|
||||
|
||||
// when the focused window has an `Unfocused` border kind, usually this happens if
|
||||
// we focus an admin window and then refocus the previously focused window. For
|
||||
// komorebi it will have the same state has before, however the previously focused
|
||||
// window changed its border to unfocused so now we need to update it again.
|
||||
if !should_process_notification
|
||||
&& window_border(notification.0.unwrap_or_default())
|
||||
.is_some_and(|b| b.window_kind == WindowKind::Unfocused)
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification && switch_focus_to_from_floating_window {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
if let Some(ref previous) = previous_notification {
|
||||
if previous.0.unwrap_or_default() != notification.0.unwrap_or_default() {
|
||||
// handle the pause edge case
|
||||
if is_paused && !previous_is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the unpause edge case
|
||||
if previous_is_paused && !is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the retile edge case
|
||||
if !should_process_notification && BORDER_STATE.lock().is_empty() {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// when we switch focus to/from a floating window
|
||||
let switch_focus_to_from_floating_window =
|
||||
floating_window_hwnds.iter().any(|fw| {
|
||||
// if we switch focus to a floating window
|
||||
fw == ¬ification_hwnd.unwrap_or_default() ||
|
||||
// if there is any floating window with a `WindowKind::Floating` border
|
||||
// that no longer is the foreground window then we need to update that
|
||||
// border.
|
||||
(fw != &foreground_window
|
||||
&& window_border(*fw)
|
||||
.is_some_and(|b| b.window_kind == WindowKind::Floating))
|
||||
});
|
||||
|
||||
// when the focused window has an `Unfocused` border kind, usually this happens if
|
||||
// we focus an admin window and then refocus the previously focused window. For
|
||||
// komorebi it will have the same state has before, however the previously focused
|
||||
// window changed its border to unfocused so now we need to update it again.
|
||||
if !should_process_notification
|
||||
&& window_border(notification_hwnd.unwrap_or_default())
|
||||
.is_some_and(|b| b.window_kind == WindowKind::Unfocused)
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification && switch_focus_to_from_floating_window {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
should_process_notification
|
||||
}
|
||||
}
|
||||
Notification::ForceUpdate => true,
|
||||
};
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
tracing::debug!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
@@ -326,8 +367,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
if !BORDER_ENABLED.load_consume()
|
||||
// Or if the wm is paused
|
||||
|| is_paused
|
||||
// Or if we are handling an alt-tab across workspaces
|
||||
|| ALT_TAB_HWND.load().is_some()
|
||||
{
|
||||
// Destroy the borders we know about
|
||||
for (_, border) in borders.drain() {
|
||||
@@ -403,11 +442,19 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
}
|
||||
|
||||
// Update the border's monitor idx in case it changed
|
||||
border.monitor_idx = Some(monitor_idx);
|
||||
|
||||
let rect = WindowsApi::window_rect(focused_window_hwnd)?;
|
||||
border.window_rect = rect;
|
||||
|
||||
if new_border {
|
||||
border.set_position(&rect, focused_window_hwnd)?;
|
||||
} else if matches!(notification, Notification::ForceUpdate) {
|
||||
// Update the border brushes if there was a forced update
|
||||
// notification and this is not a new border (new border's
|
||||
// already have their brushes updated on creation)
|
||||
border.update_brushes()?;
|
||||
}
|
||||
|
||||
border.invalidate();
|
||||
@@ -415,14 +462,40 @@ 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;
|
||||
}
|
||||
|
||||
@@ -490,7 +563,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
|| focused_window_hwnd != foreground_window
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
}
|
||||
} else if c.windows().len() > 1 {
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
@@ -517,6 +594,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
}
|
||||
|
||||
// Update the border's monitor idx in case it changed
|
||||
border.monitor_idx = Some(monitor_idx);
|
||||
|
||||
// avoid getting into a thread restart loop if we try to look up
|
||||
// rect info for a window that has been destroyed by the time
|
||||
// we get here
|
||||
@@ -529,13 +609,18 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
};
|
||||
border.window_rect = rect;
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
|
||||
let should_invalidate = new_border
|
||||
|| (last_focus_state != new_focus_state)
|
||||
|| layer_changed;
|
||||
|| 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, focused_window_hwnd)?;
|
||||
border.invalidate();
|
||||
}
|
||||
@@ -543,53 +628,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;
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
border.window_rect = rect;
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
|
||||
let should_invalidate = new_border
|
||||
|| (last_focus_state != new_focus_state)
|
||||
|| layer_changed;
|
||||
|
||||
if should_invalidate {
|
||||
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,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -605,6 +652,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
|
||||
|
||||
@@ -140,3 +140,114 @@ impl Container {
|
||||
self.windows.focus(idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_contains_window() {
|
||||
let mut container = Container::default();
|
||||
|
||||
for i in 0..3 {
|
||||
container.add_window(Window::from(i));
|
||||
}
|
||||
|
||||
// Should return true for existing windows
|
||||
assert!(container.contains_window(1));
|
||||
assert_eq!(container.idx_for_window(1), Some(1));
|
||||
|
||||
// Should return false since window 4 doesn't exist
|
||||
assert!(!container.contains_window(4));
|
||||
assert_eq!(container.idx_for_window(4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_window_by_idx() {
|
||||
let mut container = Container::default();
|
||||
|
||||
for i in 0..3 {
|
||||
container.add_window(Window::from(i));
|
||||
}
|
||||
|
||||
// Remove window 1
|
||||
container.remove_window_by_idx(1);
|
||||
|
||||
// Should only have 2 windows left
|
||||
assert_eq!(container.windows().len(), 2);
|
||||
|
||||
// Should return false since window 1 was removed
|
||||
assert!(!container.contains_window(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_focused_window() {
|
||||
let mut container = Container::default();
|
||||
|
||||
for i in 0..3 {
|
||||
container.add_window(Window::from(i));
|
||||
}
|
||||
|
||||
// Should be focused on the last created window
|
||||
assert_eq!(container.focused_window_idx(), 2);
|
||||
|
||||
// Remove the focused window
|
||||
container.remove_focused_window();
|
||||
|
||||
// Should be focused on the window before the removed one
|
||||
assert_eq!(container.focused_window_idx(), 1);
|
||||
|
||||
// Should only have 2 windows left
|
||||
assert_eq!(container.windows().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_window() {
|
||||
let mut container = Container::default();
|
||||
|
||||
container.add_window(Window::from(1));
|
||||
|
||||
assert_eq!(container.windows().len(), 1);
|
||||
assert_eq!(container.focused_window_idx(), 0);
|
||||
assert!(container.contains_window(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focus_window() {
|
||||
let mut container = Container::default();
|
||||
|
||||
for i in 0..3 {
|
||||
container.add_window(Window::from(i));
|
||||
}
|
||||
|
||||
// Should focus on the last created window
|
||||
assert_eq!(container.focused_window_idx(), 2);
|
||||
|
||||
// focus on the window at index 1
|
||||
container.focus_window(1);
|
||||
|
||||
// Should be focused on window 1
|
||||
assert_eq!(container.focused_window_idx(), 1);
|
||||
|
||||
// focus on the window at index 0
|
||||
container.focus_window(0);
|
||||
|
||||
// Should be focused on window 0
|
||||
assert_eq!(container.focused_window_idx(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idx_for_window() {
|
||||
let mut container = Container::default();
|
||||
|
||||
for i in 0..3 {
|
||||
container.add_window(Window::from(i));
|
||||
}
|
||||
|
||||
// Should return the index of the window
|
||||
assert_eq!(container.idx_for_window(1), Some(1));
|
||||
|
||||
// Should return None since window 4 doesn't exist
|
||||
assert_eq!(container.idx_for_window(4), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use clap::ValueEnum;
|
||||
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Rect;
|
||||
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,110 @@ 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);
|
||||
|
||||
match len {
|
||||
// treat < 3 windows the same as the columns layout
|
||||
len if len < 3 => {
|
||||
layouts = columns(area, len);
|
||||
|
||||
let adjustment = calculate_columns_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;
|
||||
},
|
||||
);
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
columns_reverse(&mut layouts);
|
||||
}
|
||||
}
|
||||
}
|
||||
// treat >= column_count as scrolling
|
||||
len => {
|
||||
let visible_columns = area.right / column_width;
|
||||
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 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,
|
||||
@@ -487,6 +593,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 +650,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 {
|
||||
@@ -1115,6 +1224,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;
|
||||
|
||||
@@ -21,9 +21,24 @@ 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>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ScrollingLayoutOptions {
|
||||
/// Desired number of visible columns (default: 3)
|
||||
pub columns: usize,
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
@@ -31,6 +46,7 @@ impl DefaultLayout {
|
||||
n if n > 1 => 1,
|
||||
_ => 0,
|
||||
},
|
||||
Self::Scrolling => 0,
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
@@ -53,6 +69,7 @@ impl DefaultLayout {
|
||||
_ => len.saturating_sub(1),
|
||||
},
|
||||
DefaultLayout::RightMainVerticalStack => 0,
|
||||
DefaultLayout::Scrolling => len.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +92,7 @@ impl DefaultLayout {
|
||||
| Self::RightMainVerticalStack
|
||||
| Self::HorizontalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::Scrolling
|
||||
) {
|
||||
return None;
|
||||
};
|
||||
@@ -169,13 +187,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,
|
||||
|
||||
@@ -102,6 +102,7 @@ impl Direction for DefaultLayout {
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx > 2,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Down => match self {
|
||||
Self::BSP => idx != count - 1 && idx % 2 != 0,
|
||||
@@ -111,6 +112,7 @@ impl Direction for DefaultLayout {
|
||||
Self::HorizontalStack => idx == 0,
|
||||
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
Self::BSP => idx != 0,
|
||||
@@ -120,6 +122,7 @@ impl Direction for DefaultLayout {
|
||||
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx != 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => idx != 0,
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
Self::BSP => idx % 2 == 0 && idx != count - 1,
|
||||
@@ -133,6 +136,7 @@ impl Direction for DefaultLayout {
|
||||
_ => idx < 2,
|
||||
},
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => idx != count - 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -158,6 +162,7 @@ impl Direction for DefaultLayout {
|
||||
| Self::RightMainVerticalStack => idx - 1,
|
||||
Self::HorizontalStack => 0,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +181,7 @@ impl Direction for DefaultLayout {
|
||||
Self::Columns => unreachable!(),
|
||||
Self::HorizontalStack => 1,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +209,7 @@ impl Direction for DefaultLayout {
|
||||
_ => 0,
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => idx - 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +230,7 @@ impl Direction for DefaultLayout {
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => idx + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||
|
||||
use std::path::Path;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -28,7 +27,10 @@ pub use default_layout::DefaultLayout;
|
||||
pub use direction::Direction;
|
||||
pub use layout::Layout;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use pathext::replace_env_in_path;
|
||||
pub use pathext::resolve_option_hashmap_usize_path;
|
||||
pub use pathext::PathExt;
|
||||
pub use pathext::ResolvedPathBuf;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
@@ -44,6 +46,8 @@ pub mod operation_direction;
|
||||
pub mod pathext;
|
||||
pub mod rect;
|
||||
|
||||
// serde_as must be before derive
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Display)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
@@ -62,6 +66,8 @@ pub enum SocketMessage {
|
||||
UnstackAll,
|
||||
ResizeWindowEdge(OperationDirection, Sizing),
|
||||
ResizeWindowAxis(Axis, Sizing),
|
||||
MoveContainerToLastWorkspace,
|
||||
SendContainerToLastWorkspace,
|
||||
MoveContainerToMonitorNumber(usize),
|
||||
CycleMoveContainerToMonitor(CycleDirection),
|
||||
MoveContainerToWorkspaceNumber(usize),
|
||||
@@ -84,6 +90,9 @@ pub enum SocketMessage {
|
||||
PromoteFocus,
|
||||
PromoteWindow(OperationDirection),
|
||||
EagerFocus(String),
|
||||
LockMonitorWorkspaceContainer(usize, usize, usize),
|
||||
UnlockMonitorWorkspaceContainer(usize, usize, usize),
|
||||
ToggleLock,
|
||||
ToggleFloat,
|
||||
ToggleMonocle,
|
||||
ToggleMaximize,
|
||||
@@ -100,7 +109,8 @@ pub enum SocketMessage {
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(DefaultLayout),
|
||||
CycleLayout(CycleDirection),
|
||||
ChangeLayoutCustom(PathBuf),
|
||||
ScrollingLayoutColumns(NonZeroUsize),
|
||||
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
FlipLayout(Axis),
|
||||
ToggleWorkspaceWindowContainerBehaviour,
|
||||
ToggleWorkspaceFloatOverride,
|
||||
@@ -118,8 +128,8 @@ pub enum SocketMessage {
|
||||
RetileWithResizeDimensions,
|
||||
QuickSave,
|
||||
QuickLoad,
|
||||
Save(PathBuf),
|
||||
Load(PathBuf),
|
||||
Save(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
Load(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
CycleFocusMonitor(CycleDirection),
|
||||
CycleFocusWorkspace(CycleDirection),
|
||||
CycleFocusEmptyWorkspace(CycleDirection),
|
||||
@@ -142,23 +152,28 @@ pub enum SocketMessage {
|
||||
WorkspaceName(usize, usize, String),
|
||||
WorkspaceLayout(usize, usize, DefaultLayout),
|
||||
NamedWorkspaceLayout(String, DefaultLayout),
|
||||
WorkspaceLayoutCustom(usize, usize, PathBuf),
|
||||
NamedWorkspaceLayoutCustom(String, PathBuf),
|
||||
WorkspaceLayoutCustom(usize, usize, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
NamedWorkspaceLayoutCustom(String, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
WorkspaceLayoutRule(usize, usize, usize, DefaultLayout),
|
||||
NamedWorkspaceLayoutRule(String, usize, DefaultLayout),
|
||||
WorkspaceLayoutCustomRule(usize, usize, usize, PathBuf),
|
||||
NamedWorkspaceLayoutCustomRule(String, usize, PathBuf),
|
||||
WorkspaceLayoutCustomRule(
|
||||
usize,
|
||||
usize,
|
||||
usize,
|
||||
#[serde_as(as = "ResolvedPathBuf")] PathBuf,
|
||||
),
|
||||
NamedWorkspaceLayoutCustomRule(String, usize, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
ClearWorkspaceLayoutRules(usize, usize),
|
||||
ClearNamedWorkspaceLayoutRules(String),
|
||||
ToggleWorkspaceLayer,
|
||||
// Configuration
|
||||
ReloadConfiguration,
|
||||
ReplaceConfiguration(PathBuf),
|
||||
ReloadStaticConfiguration(PathBuf),
|
||||
ReplaceConfiguration(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
ReloadStaticConfiguration(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
WatchConfiguration(bool),
|
||||
CompleteConfiguration,
|
||||
AltFocusHack(bool),
|
||||
Theme(KomorebiTheme),
|
||||
Theme(Box<KomorebiTheme>),
|
||||
Animation(bool, Option<AnimationPrefix>),
|
||||
AnimationDuration(u64, Option<AnimationPrefix>),
|
||||
AnimationFps(u64),
|
||||
@@ -197,6 +212,9 @@ pub enum SocketMessage {
|
||||
ClearNamedWorkspaceRules(String),
|
||||
ClearAllWorkspaceRules,
|
||||
EnforceWorkspaceRules,
|
||||
SessionFloatRule,
|
||||
SessionFloatRules,
|
||||
ClearSessionFloatRules,
|
||||
#[serde(alias = "FloatRule")]
|
||||
IgnoreRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
@@ -312,6 +330,7 @@ pub enum WindowKind {
|
||||
Monocle,
|
||||
#[default]
|
||||
Unfocused,
|
||||
UnfocusedLocked,
|
||||
Floating,
|
||||
}
|
||||
|
||||
@@ -323,6 +342,9 @@ pub enum StateQuery {
|
||||
FocusedContainerIndex,
|
||||
FocusedWindowIndex,
|
||||
FocusedWorkspaceName,
|
||||
FocusedWorkspaceLayout,
|
||||
FocusedContainerKind,
|
||||
Version,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -358,6 +380,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(
|
||||
@@ -372,10 +409,64 @@ pub enum WindowContainerBehaviour {
|
||||
Append,
|
||||
}
|
||||
|
||||
#[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 FloatingLayerBehaviour {
|
||||
/// Tile new windows (unless they match a float rule or float override is active)
|
||||
#[default]
|
||||
Tile,
|
||||
/// Float new windows
|
||||
Float,
|
||||
}
|
||||
|
||||
#[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,
|
||||
@@ -383,19 +474,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,
|
||||
@@ -403,10 +497,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,
|
||||
@@ -435,45 +532,38 @@ impl Sizing {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_home_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
|
||||
let mut resolved_path = PathBuf::new();
|
||||
let mut resolved = false;
|
||||
for c in path.as_ref().components() {
|
||||
match c {
|
||||
std::path::Component::Normal(c)
|
||||
if (c == "~" || c == "$Env:USERPROFILE" || c == "$HOME") && !resolved =>
|
||||
{
|
||||
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
|
||||
|
||||
resolved_path.extend(home.components());
|
||||
resolved = true;
|
||||
}
|
||||
|
||||
std::path::Component::Normal(c) if (c == "$Env:KOMOREBI_CONFIG_HOME") && !resolved => {
|
||||
let komorebi_config_home =
|
||||
PathBuf::from(std::env::var("KOMOREBI_CONFIG_HOME").ok().ok_or_else(|| {
|
||||
anyhow!("there is no KOMOREBI_CONFIG_HOME environment variable set")
|
||||
})?);
|
||||
|
||||
resolved_path.extend(komorebi_config_home.components());
|
||||
resolved = true;
|
||||
}
|
||||
|
||||
_ => resolved_path.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
let parent = resolved_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("cannot parse parent directory"))?;
|
||||
|
||||
Ok(if parent.is_dir() {
|
||||
let file = resolved_path
|
||||
.components()
|
||||
.last()
|
||||
.ok_or_else(|| anyhow!("cannot parse filename"))?;
|
||||
dunce::canonicalize(parent)?.join(file)
|
||||
} else {
|
||||
resolved_path
|
||||
})
|
||||
#[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::*;
|
||||
|
||||
#[test]
|
||||
fn deserializes() {
|
||||
// Set a variable for testing
|
||||
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();
|
||||
|
||||
let SocketMessage::WorkspaceLayoutCustomRule(
|
||||
_workspace_index,
|
||||
_workspace_number,
|
||||
_monitor_index,
|
||||
path,
|
||||
) = message
|
||||
else {
|
||||
panic!("Expected WorkspaceLayoutCustomRule");
|
||||
};
|
||||
|
||||
assert_eq!(path, PathBuf::from("/path/VALUE/d"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,192 @@
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Path extension trait
|
||||
pub trait PathExt {
|
||||
/// Resolve environment variable components in a path.
|
||||
///
|
||||
/// Resolves the following formats:
|
||||
/// - CMD: `%variable%`
|
||||
/// - PowerShell: `$Env:variable`
|
||||
/// - Bash: `$variable`.
|
||||
fn replace_env(&self) -> PathBuf;
|
||||
}
|
||||
|
||||
impl PathExt for PathBuf {
|
||||
/// Blanket implementation for all types that can be converted to a `Path`.
|
||||
impl<P: AsRef<Path>> PathExt for P {
|
||||
fn replace_env(&self) -> PathBuf {
|
||||
let mut result = PathBuf::new();
|
||||
let mut out = PathBuf::new();
|
||||
|
||||
for component in self.components() {
|
||||
match component {
|
||||
Component::Normal(segment) => {
|
||||
// Check if it starts with `$` or `$Env:`
|
||||
if let Some(stripped_segment) = segment.to_string_lossy().strip_prefix('$') {
|
||||
let var_name = if let Some(env_name) = stripped_segment.strip_prefix("Env:")
|
||||
{
|
||||
// Extract the variable name after `$Env:`
|
||||
env_name
|
||||
} else if stripped_segment == "HOME" {
|
||||
// Special case for `$HOME`
|
||||
"USERPROFILE"
|
||||
} else {
|
||||
// Extract the variable name after `$`
|
||||
stripped_segment
|
||||
};
|
||||
|
||||
if let Ok(value) = env::var(var_name) {
|
||||
result.push(&value); // Replace with the value
|
||||
} else {
|
||||
result.push(segment); // Keep as-is if variable is not found
|
||||
}
|
||||
} else {
|
||||
result.push(segment); // Keep as-is if not an environment variable
|
||||
for c in self.as_ref().components() {
|
||||
match c {
|
||||
Component::Normal(mut c) => {
|
||||
// Special case for ~ and $HOME, replace with $Env:USERPROFILE
|
||||
if c == OsStr::new("~") || c.eq_ignore_ascii_case("$HOME") {
|
||||
c = OsStr::new("$Env:USERPROFILE");
|
||||
}
|
||||
|
||||
let bytes = c.as_encoded_bytes();
|
||||
|
||||
// %LOCALAPPDATA%
|
||||
let var = if bytes[0] == b'%' && bytes[bytes.len() - 1] == b'%' {
|
||||
Some(&bytes[1..bytes.len() - 1])
|
||||
} else {
|
||||
// prefix length is 5 for $Env: and 1 for $
|
||||
// so we take the minimum of 5 and the length of the bytes
|
||||
let prefix = &bytes[..5.min(bytes.len())];
|
||||
let prefix = unsafe { OsStr::from_encoded_bytes_unchecked(prefix) };
|
||||
|
||||
// $Env:LOCALAPPDATA
|
||||
if prefix.eq_ignore_ascii_case("$Env:") {
|
||||
Some(&bytes[5..])
|
||||
} else if bytes[0] == b'$' {
|
||||
// $LOCALAPPDATA
|
||||
Some(&bytes[1..])
|
||||
} else {
|
||||
// not a variable
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// 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) {
|
||||
out.push(value);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if not a variable, or a value couldn't be obtained from environemnt
|
||||
// then push the component as is
|
||||
out.push(c);
|
||||
}
|
||||
_ => {
|
||||
// Add other components (e.g., root, parent) as-is
|
||||
result.push(component.as_os_str());
|
||||
}
|
||||
|
||||
// other components are pushed as is
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace environment variables in a path. This is a wrapper around
|
||||
/// [`PathExt::replace_env`] to be used in Clap arguments parsing.
|
||||
pub fn replace_env_in_path(input: &str) -> Result<PathBuf, std::convert::Infallible> {
|
||||
Ok(input.replace_env())
|
||||
}
|
||||
|
||||
/// A wrapper around [`PathBuf`] that has a custom [Deserialize] implementation
|
||||
/// that uses [`PathExt::replace_env`] to resolve environment variables
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ResolvedPathBuf(PathBuf);
|
||||
|
||||
impl ResolvedPathBuf {
|
||||
/// Create a new [`ResolvedPathBuf`] from a [`PathBuf`]
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self(path.replace_env())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResolvedPathBuf> for PathBuf {
|
||||
fn from(path: ResolvedPathBuf) -> Self {
|
||||
path.0
|
||||
}
|
||||
}
|
||||
|
||||
impl serde_with::SerializeAs<PathBuf> for ResolvedPathBuf {
|
||||
fn serialize_as<S>(path: &PathBuf, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
path.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde_with::DeserializeAs<'de, PathBuf> for ResolvedPathBuf {
|
||||
fn deserialize_as<D>(deserializer: D) -> Result<PathBuf, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let path = PathBuf::deserialize(deserializer)?;
|
||||
Ok(path.replace_env())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl serde_with::schemars_0_8::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
|
||||
fn schema_name() -> String {
|
||||
"PathBuf".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
<PathBuf as schemars::JsonSchema>::json_schema(gen)
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom deserializer for [`Option<HashMap<usize, PathBuf>>`] that uses
|
||||
/// [`PathExt::replace_env`] to resolve environment variables in the paths.
|
||||
///
|
||||
/// This is used in `WorkspaceConfig` struct because we can't use
|
||||
/// #[serde_with::serde_as] as it doesn't handle [`Option<HashMap<usize, ResolvedPathBuf>>`]
|
||||
/// quite well and generated compiler errors that can't be fixed because of Rust's orphan rule.
|
||||
pub fn resolve_option_hashmap_usize_path<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<HashMap<usize, PathBuf>>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let map = Option::<HashMap<usize, PathBuf>>::deserialize(deserializer)?;
|
||||
Ok(map.map(|map| map.into_iter().map(|(k, v)| (k, v.replace_env())).collect()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// helper functions
|
||||
fn expected<P: AsRef<Path>>(p: P) -> PathBuf {
|
||||
// Ensure that the path is using the correct path separator for the OS.
|
||||
p.as_ref().components().collect::<PathBuf>()
|
||||
}
|
||||
|
||||
fn resolve<P: AsRef<Path>>(p: P) -> PathBuf {
|
||||
p.replace_env()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_env_vars() {
|
||||
// Set a variable for testing
|
||||
std::env::set_var("VAR", "VALUE");
|
||||
|
||||
// %VAR% format
|
||||
assert_eq!(resolve("/path/%VAR%/d"), expected("/path/VALUE/d"));
|
||||
// $env:VAR format
|
||||
assert_eq!(resolve("/path/$env:VAR/d"), expected("/path/VALUE/d"));
|
||||
// $VAR format
|
||||
assert_eq!(resolve("/path/$VAR/d"), expected("/path/VALUE/d"));
|
||||
|
||||
// non-existent variable
|
||||
assert_eq!(resolve("/path/%ASD%/to/d"), expected("/path/%ASD%/to/d"));
|
||||
assert_eq!(
|
||||
resolve("/path/$env:ASD/to/d"),
|
||||
expected("/path/$env:ASD/to/d")
|
||||
);
|
||||
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");
|
||||
|
||||
// ~ and $HOME should be replaced with $Env:USERPROFILE
|
||||
assert_eq!(resolve("~"), expected("C:\\Users\\user"));
|
||||
assert_eq!(resolve("$HOME"), 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 {
|
||||
|
||||
@@ -5,10 +5,10 @@ pub mod border_manager;
|
||||
pub mod com;
|
||||
#[macro_use]
|
||||
pub mod ring;
|
||||
pub mod colour;
|
||||
pub mod container;
|
||||
pub mod core;
|
||||
pub mod focus_manager;
|
||||
pub mod locked_deque;
|
||||
pub mod monitor;
|
||||
pub mod monitor_reconciliator;
|
||||
pub mod process_command;
|
||||
@@ -29,7 +29,6 @@ pub mod windows_callbacks;
|
||||
pub mod winevent;
|
||||
pub mod winevent_listener;
|
||||
pub mod workspace;
|
||||
pub mod workspace_reconciliator;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use monitor_reconciliator::MonitorNotification;
|
||||
@@ -47,11 +46,12 @@ use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use colour::*;
|
||||
pub use core::*;
|
||||
pub use komorebi_themes::colour::*;
|
||||
pub use process_command::*;
|
||||
pub use process_event::*;
|
||||
pub use static_config::*;
|
||||
pub use win32_display_data;
|
||||
pub use window::*;
|
||||
pub use window_manager::*;
|
||||
pub use window_manager_event::*;
|
||||
@@ -63,8 +63,10 @@ use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use crate::core::config_generation::WorkspaceMatchingRule;
|
||||
use color_eyre::Result;
|
||||
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;
|
||||
@@ -131,8 +133,8 @@ lazy_static! {
|
||||
static ref TRANSPARENCY_BLACKLIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref DISPLAY_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, String>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref DISPLAY_INDEX_PREFERENCES: Arc<RwLock<HashMap<usize, String>>> =
|
||||
Arc::new(RwLock::new(HashMap::new()));
|
||||
static ref WORKSPACE_MATCHING_RULES: Arc<Mutex<Vec<WorkspaceMatchingRule>>> =
|
||||
Arc::new(Mutex::new(Vec::new()));
|
||||
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
|
||||
@@ -157,7 +159,15 @@ lazy_static! {
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
]));
|
||||
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
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![
|
||||
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(),
|
||||
]));
|
||||
@@ -172,6 +182,8 @@ lazy_static! {
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
]));
|
||||
static ref DUPLICATE_MONITOR_SERIAL_IDS: Arc<RwLock<Vec<String>>> =
|
||||
Arc::new(RwLock::new(Vec::new()));
|
||||
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
|
||||
@@ -184,15 +196,16 @@ lazy_static! {
|
||||
Arc::new(Mutex::new(HidingBehaviour::Cloak));
|
||||
pub static ref HOME_DIR: PathBuf = {
|
||||
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
let home = home_path.replace_env();
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!(
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
);
|
||||
|
||||
|
||||
home
|
||||
})
|
||||
};
|
||||
pub static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
|
||||
@@ -222,6 +235,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);
|
||||
@@ -235,6 +250,11 @@ 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);
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
@@ -286,6 +306,14 @@ pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
Monitor(MonitorNotification),
|
||||
VirtualDesktop(VirtualDesktopNotification),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum VirtualDesktopNotification {
|
||||
EnteredAssociatedVirtualDesktop,
|
||||
LeftAssociatedVirtualDesktop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
316
komorebi/src/locked_deque.rs
Normal file
316
komorebi/src/locked_deque.rs
Normal file
@@ -0,0 +1,316 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,18 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_utils::Backoff;
|
||||
use komorebi::animation::AnimationEngine;
|
||||
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
use komorebi::replace_env_in_path;
|
||||
#[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;
|
||||
@@ -48,16 +52,13 @@ use komorebi::window_manager::State;
|
||||
use komorebi::window_manager::WindowManager;
|
||||
use komorebi::windows_api::WindowsApi;
|
||||
use komorebi::winevent_listener;
|
||||
use komorebi::workspace_reconciliator;
|
||||
use komorebi::CUSTOM_FFM;
|
||||
use komorebi::DATA_DIR;
|
||||
use komorebi::HOME_DIR;
|
||||
use komorebi::INITIAL_CONFIGURATION_LOADED;
|
||||
use komorebi::SESSION_ID;
|
||||
|
||||
shadow_rs::shadow!(build);
|
||||
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
fn setup(log_level: LogLevel) -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
@@ -65,7 +66,16 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
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");
|
||||
@@ -143,8 +153,19 @@ fn detect_deadlocks() {
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, ValueEnum, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
#[default]
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
||||
#[clap(author, about, version = komorebi::build::CLAP_LONG_VERSION)]
|
||||
struct Opts {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(short, long = "ffm")]
|
||||
@@ -157,10 +178,14 @@ struct Opts {
|
||||
tcp_port: Option<usize>,
|
||||
/// Path to a static configuration JSON file
|
||||
#[clap(short, long)]
|
||||
#[clap(value_parser = replace_env_in_path)]
|
||||
config: Option<PathBuf>,
|
||||
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
|
||||
#[clap(long)]
|
||||
clean_state: bool,
|
||||
/// Level of log output verbosity
|
||||
#[clap(long, value_enum, default_value_t=LogLevel::Info)]
|
||||
log_level: LogLevel,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -169,8 +194,28 @@ fn main() -> 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 {
|
||||
return Err(anyhow!(
|
||||
"failed call to AllowSetForegroundWindow after 5 retries"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
WindowsApi::set_process_dpi_awareness_context()?;
|
||||
|
||||
let session_id = WindowsApi::process_id_to_session_id()?;
|
||||
@@ -198,7 +243,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
// File logging worker guard has to have an assignment in the main fn to work
|
||||
let (_guard, _color_guard) = setup()?;
|
||||
let (_guard, _color_guard) = setup(opts.log_level)?;
|
||||
|
||||
WindowsApi::foreground_lock_timeout()?;
|
||||
|
||||
@@ -278,7 +323,6 @@ fn main() -> Result<()> {
|
||||
border_manager::listen_for_notifications(wm.clone());
|
||||
stackbar_manager::listen_for_notifications(wm.clone());
|
||||
transparency_manager::listen_for_notifications(wm.clone());
|
||||
workspace_reconciliator::listen_for_notifications(wm.clone());
|
||||
monitor_reconciliator::listen_for_notifications(wm.clone())?;
|
||||
reaper::listen_for_notifications(wm.clone(), wm.lock().known_hwnds.clone());
|
||||
focus_manager::listen_for_notifications(wm.clone());
|
||||
|
||||
@@ -12,14 +12,21 @@ use getset::Setters;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::border_manager::BORDER_ENABLED;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::core::Rect;
|
||||
|
||||
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::FloatingLayerBehaviour;
|
||||
use crate::Layout;
|
||||
use crate::OperationDirection;
|
||||
use crate::Wallpaper;
|
||||
use crate::WindowsApi;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
@@ -59,6 +66,10 @@ pub struct Monitor {
|
||||
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>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||
}
|
||||
|
||||
impl_ring_elements!(Monitor, Workspace);
|
||||
@@ -114,6 +125,8 @@ pub fn new(
|
||||
workspace_names: HashMap::default(),
|
||||
container_padding: None,
|
||||
workspace_padding: None,
|
||||
wallpaper: None,
|
||||
floating_layer_behaviour: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +168,8 @@ impl Monitor {
|
||||
workspace_names: Default::default(),
|
||||
container_padding: None,
|
||||
workspace_padding: None,
|
||||
wallpaper: None,
|
||||
floating_layer_behaviour: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,11 +179,23 @@ impl Monitor {
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
pub fn focused_workspace_layout(&self) -> Option<Layout> {
|
||||
self.focused_workspace().and_then(|workspace| {
|
||||
if *workspace.tile() {
|
||||
Some(workspace.layout().clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
||||
let focused_idx = self.focused_workspace_idx();
|
||||
let hmonitor = self.id();
|
||||
let monitor_wp = self.wallpaper.clone();
|
||||
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
|
||||
if i == focused_idx {
|
||||
workspace.restore(mouse_follows_focus)?;
|
||||
workspace.restore(mouse_follows_focus, hmonitor, &monitor_wp)?;
|
||||
} else {
|
||||
workspace.hide(None);
|
||||
}
|
||||
@@ -185,18 +212,34 @@ impl Monitor {
|
||||
let workspace_padding = self
|
||||
.workspace_padding()
|
||||
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
|
||||
let (border_width, border_offset) = {
|
||||
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
|
||||
if border_enabled {
|
||||
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
(border_width, border_offset)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
};
|
||||
let work_area = *self.work_area_size();
|
||||
let offset = self.work_area_offset.or(offset);
|
||||
let work_area_offset = self.work_area_offset.or(offset);
|
||||
let window_based_work_area_offset = self.window_based_work_area_offset();
|
||||
let limit = self.window_based_work_area_offset_limit();
|
||||
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit();
|
||||
let floating_layer_behaviour = self.floating_layer_behaviour();
|
||||
|
||||
for workspace in self.workspaces_mut() {
|
||||
workspace.globals_mut().container_padding = container_padding;
|
||||
workspace.globals_mut().workspace_padding = workspace_padding;
|
||||
workspace.globals_mut().work_area = work_area;
|
||||
workspace.globals_mut().work_area_offset = offset;
|
||||
workspace.globals_mut().window_based_work_area_offset = window_based_work_area_offset;
|
||||
workspace.globals_mut().window_based_work_area_offset_limit = limit;
|
||||
workspace.globals = WorkspaceGlobals {
|
||||
container_padding,
|
||||
workspace_padding,
|
||||
border_width,
|
||||
border_offset,
|
||||
work_area,
|
||||
work_area_offset,
|
||||
window_based_work_area_offset,
|
||||
window_based_work_area_offset_limit,
|
||||
floating_layer_behaviour,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,18 +251,34 @@ impl Monitor {
|
||||
let workspace_padding = self
|
||||
.workspace_padding()
|
||||
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
|
||||
let (border_width, border_offset) = {
|
||||
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
|
||||
if border_enabled {
|
||||
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
(border_width, border_offset)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
};
|
||||
let work_area = *self.work_area_size();
|
||||
let offset = self.work_area_offset.or(offset);
|
||||
let work_area_offset = self.work_area_offset.or(offset);
|
||||
let window_based_work_area_offset = self.window_based_work_area_offset();
|
||||
let limit = self.window_based_work_area_offset_limit();
|
||||
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit();
|
||||
let floating_layer_behaviour = self.floating_layer_behaviour();
|
||||
|
||||
if let Some(workspace) = self.workspaces_mut().get_mut(workspace_idx) {
|
||||
workspace.globals_mut().container_padding = container_padding;
|
||||
workspace.globals_mut().workspace_padding = workspace_padding;
|
||||
workspace.globals_mut().work_area = work_area;
|
||||
workspace.globals_mut().work_area_offset = offset;
|
||||
workspace.globals_mut().window_based_work_area_offset = window_based_work_area_offset;
|
||||
workspace.globals_mut().window_based_work_area_offset_limit = limit;
|
||||
workspace.globals = WorkspaceGlobals {
|
||||
container_padding,
|
||||
workspace_padding,
|
||||
border_width,
|
||||
border_offset,
|
||||
work_area,
|
||||
work_area_offset,
|
||||
window_based_work_area_offset,
|
||||
window_based_work_area_offset_limit,
|
||||
floating_layer_behaviour,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,19 +428,20 @@ impl Monitor {
|
||||
.position(|w| w.hwnd == foreground_hwnd);
|
||||
|
||||
if let Some(idx) = floating_window_index {
|
||||
let window = workspace.floating_windows_mut().remove(idx);
|
||||
if let Some(window) = workspace.floating_windows_mut().remove(idx) {
|
||||
let workspaces = self.workspaces_mut();
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
target_workspace.floating_windows_mut().push(window);
|
||||
target_workspace.floating_windows_mut().push_back(window);
|
||||
target_workspace.set_layer(WorkspaceLayer::Floating);
|
||||
}
|
||||
} else {
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
@@ -398,6 +458,20 @@ impl Monitor {
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
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.set_layer(WorkspaceLayer::Tiling);
|
||||
|
||||
if let Some(direction) = direction {
|
||||
self.add_container_with_direction(
|
||||
container,
|
||||
@@ -464,3 +538,311 @@ impl Monitor {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add_container() {
|
||||
let mut m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Add container to the default workspace
|
||||
m.add_container(Container::default(), Some(0)).unwrap();
|
||||
|
||||
// Should contain a container in the current focused workspace
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
assert_eq!(workspace.containers().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_workspace_by_idx() {
|
||||
let mut m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
let new_workspace_index = m.new_workspace_idx();
|
||||
assert_eq!(new_workspace_index, 1);
|
||||
|
||||
// Create workspace 2
|
||||
m.focus_workspace(new_workspace_index).unwrap();
|
||||
|
||||
// Should have 2 workspaces
|
||||
assert_eq!(m.workspaces().len(), 2);
|
||||
|
||||
// Create workspace 3
|
||||
m.focus_workspace(new_workspace_index + 1).unwrap();
|
||||
|
||||
// Should have 3 workspaces
|
||||
assert_eq!(m.workspaces().len(), 3);
|
||||
|
||||
// Remove workspace 1
|
||||
m.remove_workspace_by_idx(1);
|
||||
|
||||
// Should have only 2 workspaces
|
||||
assert_eq!(m.workspaces().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_workspaces() {
|
||||
let mut m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
let new_workspace_index = m.new_workspace_idx();
|
||||
assert_eq!(new_workspace_index, 1);
|
||||
|
||||
// Create workspace 2
|
||||
m.focus_workspace(new_workspace_index).unwrap();
|
||||
|
||||
// Should have 2 workspaces
|
||||
assert_eq!(m.workspaces().len(), 2);
|
||||
|
||||
// Create workspace 3
|
||||
m.focus_workspace(new_workspace_index + 1).unwrap();
|
||||
|
||||
// Should have 3 workspaces
|
||||
assert_eq!(m.workspaces().len(), 3);
|
||||
|
||||
// Remove all workspaces
|
||||
m.remove_workspaces();
|
||||
|
||||
// All workspaces should be removed
|
||||
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(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
let new_workspace_index = m.new_workspace_idx();
|
||||
assert_eq!(new_workspace_index, 1);
|
||||
|
||||
// Focus workspace 2
|
||||
m.focus_workspace(new_workspace_index).unwrap();
|
||||
|
||||
// Should have 2 workspaces
|
||||
assert_eq!(m.workspaces().len(), 2);
|
||||
|
||||
// Should be focused on workspace 2
|
||||
assert_eq!(m.focused_workspace_idx(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_workspace_idx() {
|
||||
let m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
let new_workspace_index = m.new_workspace_idx();
|
||||
|
||||
// Should be the last workspace index: 1
|
||||
assert_eq!(new_workspace_index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_container_to_workspace() {
|
||||
let mut m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
let new_workspace_index = m.new_workspace_idx();
|
||||
assert_eq!(new_workspace_index, 1);
|
||||
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Create and focus workspace 2
|
||||
m.focus_workspace(new_workspace_index).unwrap();
|
||||
|
||||
// Focus workspace 1
|
||||
m.focus_workspace(0).unwrap();
|
||||
|
||||
// Move container to workspace 2
|
||||
m.move_container_to_workspace(1, true, None).unwrap();
|
||||
|
||||
// Should be focused on workspace 2
|
||||
assert_eq!(m.focused_workspace_idx(), 1);
|
||||
|
||||
// Workspace 2 should have 1 container now
|
||||
assert_eq!(m.focused_workspace().unwrap().containers().len(), 1);
|
||||
|
||||
// Move to workspace 1
|
||||
m.focus_workspace(0).unwrap();
|
||||
|
||||
// Workspace 1 should have 2 containers
|
||||
assert_eq!(m.focused_workspace().unwrap().containers().len(), 2);
|
||||
|
||||
// Move a another container from workspace 1 to workspace 2 without following
|
||||
m.move_container_to_workspace(1, false, None).unwrap();
|
||||
|
||||
// Should have 1 container
|
||||
assert_eq!(m.focused_workspace().unwrap().containers().len(), 1);
|
||||
|
||||
// Should still be focused on workspace 1
|
||||
assert_eq!(m.focused_workspace_idx(), 0);
|
||||
|
||||
// Switch to workspace 2
|
||||
m.focus_workspace(1).unwrap();
|
||||
|
||||
// Workspace 2 should now have 2 containers
|
||||
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(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Create and focus another workspace
|
||||
let new_workspace_index = m.new_workspace_idx();
|
||||
m.focus_workspace(new_workspace_index).unwrap();
|
||||
|
||||
// Should have 2 workspaces now
|
||||
assert_eq!(m.workspaces().len(), 2, "Monitor should have 2 workspaces");
|
||||
|
||||
// Ensure the monitor has at least 5 workspaces
|
||||
m.ensure_workspace_count(5);
|
||||
|
||||
// Monitor should have 5 workspaces
|
||||
assert_eq!(m.workspaces().len(), 5, "Monitor should have 5 workspaces");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_workspace_count_only_default_workspace() {
|
||||
let mut m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Ensure the monitor has at least 5 workspaces
|
||||
m.ensure_workspace_count(5);
|
||||
|
||||
// Monitor should have 5 workspaces
|
||||
assert_eq!(m.workspaces().len(), 5, "Monitor should have 5 workspaces");
|
||||
|
||||
// Try to call the ensure workspace count again to ensure it doesn't change
|
||||
m.ensure_workspace_count(3);
|
||||
assert_eq!(m.workspaces().len(), 5, "Monitor should have 5 workspaces");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
@@ -27,7 +29,7 @@ use std::sync::OnceLock;
|
||||
|
||||
pub mod hidden;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum MonitorNotification {
|
||||
@@ -66,17 +68,51 @@ pub fn send_notification(notification: MonitorNotification) {
|
||||
}
|
||||
|
||||
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
|
||||
let dip = DISPLAY_INDEX_PREFERENCES.read();
|
||||
let mut dip_ids = dip.values();
|
||||
let preferred_id = if dip_ids.any(|id| id == monitor.device_id()) {
|
||||
monitor.device_id().clone()
|
||||
} else if dip_ids.any(|id| Some(id) == monitor.serial_number_id().as_ref()) {
|
||||
monitor.serial_number_id().clone().unwrap_or_default()
|
||||
} else {
|
||||
serial_or_device_id.to_string()
|
||||
};
|
||||
let mut monitor_cache = MONITOR_CACHE
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock();
|
||||
|
||||
monitor_cache.insert(serial_or_device_id.to_string(), monitor);
|
||||
monitor_cache.insert(preferred_id, monitor);
|
||||
}
|
||||
|
||||
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
|
||||
Ok(win32_display_data::connected_displays_all()
|
||||
.flatten()
|
||||
.map(|display| {
|
||||
pub fn attached_display_devices<F, I>(display_provider: F) -> color_eyre::Result<Vec<Monitor>>
|
||||
where
|
||||
F: Fn() -> I + Copy,
|
||||
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
|
||||
{
|
||||
let all_displays = display_provider().flatten().collect::<Vec<_>>();
|
||||
|
||||
let mut serial_id_map = HashMap::new();
|
||||
|
||||
for d in &all_displays {
|
||||
if let Some(id) = &d.serial_number_id {
|
||||
*serial_id_map.entry(id.clone()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for d in &all_displays {
|
||||
if let Some(id) = &d.serial_number_id {
|
||||
if serial_id_map.get(id).copied().unwrap_or_default() > 1 {
|
||||
let mut dupes = DUPLICATE_MONITOR_SERIAL_IDS.write();
|
||||
if !dupes.contains(id) {
|
||||
(*dupes).push(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_displays
|
||||
.into_iter()
|
||||
.map(|mut display| {
|
||||
let path = display.device_path;
|
||||
|
||||
let (device, device_id) = if path.is_empty() {
|
||||
@@ -93,6 +129,13 @@ pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
|
||||
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
|
||||
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
|
||||
|
||||
if let Some(id) = &display.serial_number_id {
|
||||
let dupes = DUPLICATE_MONITOR_SERIAL_IDS.read();
|
||||
if dupes.contains(id) {
|
||||
display.serial_number_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
monitor::new(
|
||||
display.hmonitor,
|
||||
display.size.into(),
|
||||
@@ -113,7 +156,7 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Re
|
||||
tracing::info!("created hidden window to listen for monitor-related events");
|
||||
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
match handle_notifications(wm.clone(), win32_display_data::connected_displays_all) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
@@ -130,7 +173,14 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
pub fn handle_notifications<F, I>(
|
||||
wm: Arc<Mutex<WindowManager>>,
|
||||
display_provider: F,
|
||||
) -> color_eyre::Result<()>
|
||||
where
|
||||
F: Fn() -> I + Copy,
|
||||
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
|
||||
{
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
@@ -255,14 +305,20 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let initial_monitor_count = wm.monitors().len();
|
||||
|
||||
// Get the currently attached display devices
|
||||
let attached_devices = attached_display_devices()?;
|
||||
let attached_devices = attached_display_devices(display_provider)?;
|
||||
|
||||
// Make sure that in our state any attached displays have the latest Win32 data
|
||||
for monitor in wm.monitors_mut() {
|
||||
for attached in &attached_devices {
|
||||
if attached.serial_number_id().eq(monitor.serial_number_id())
|
||||
|| attached.device_id().eq(monitor.device_id())
|
||||
let serial_number_ids_match = if let (Some(attached_snid), Some(m_snid)) =
|
||||
(attached.serial_number_id(), monitor.serial_number_id())
|
||||
{
|
||||
attached_snid.eq(m_snid)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if serial_number_ids_match || attached.device_id().eq(monitor.device_id()) {
|
||||
monitor.set_id(attached.id());
|
||||
monitor.set_device(attached.device().clone());
|
||||
monitor.set_device_id(attached.device_id().clone());
|
||||
@@ -380,8 +436,18 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
workspace_rules.remove(i);
|
||||
}
|
||||
|
||||
// Let's add their state to the cache for later
|
||||
monitor_cache.insert(id, m.clone());
|
||||
// Let's add their state to the cache for later, make sure to use what
|
||||
// the user set as preference as the id.
|
||||
let dip = DISPLAY_INDEX_PREFERENCES.read();
|
||||
let mut dip_ids = dip.values();
|
||||
let preferred_id = if dip_ids.any(|id| id == m.device_id()) {
|
||||
m.device_id().clone()
|
||||
} else if dip_ids.any(|id| Some(id) == m.serial_number_id().as_ref()) {
|
||||
m.serial_number_id().clone().unwrap_or_default()
|
||||
} else {
|
||||
id
|
||||
};
|
||||
monitor_cache.insert(preferred_id, m.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,6 +553,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
workspace_names: cached.workspace_names.clone(),
|
||||
container_padding: cached.container_padding,
|
||||
workspace_padding: cached.workspace_padding,
|
||||
wallpaper: cached.wallpaper.clone(),
|
||||
floating_layer_behaviour: cached.floating_layer_behaviour,
|
||||
};
|
||||
|
||||
let focused_workspace_idx = m.focused_workspace_idx();
|
||||
@@ -661,3 +729,304 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crossbeam_channel::bounded;
|
||||
use crossbeam_channel::Sender;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
use windows::Win32::Devices::Display::DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY;
|
||||
// NOTE: Using RECT instead of RECT since I get a mismatched type error. Can be updated if
|
||||
// needed.
|
||||
use windows::Win32::Foundation::RECT;
|
||||
|
||||
// Creating a Mock Display Provider
|
||||
#[derive(Clone)]
|
||||
struct MockDevice {
|
||||
hmonitor: isize,
|
||||
device_path: String,
|
||||
device_name: String,
|
||||
device_description: String,
|
||||
serial_number_id: Option<String>,
|
||||
size: RECT,
|
||||
work_area_size: RECT,
|
||||
device_key: String,
|
||||
output_technology: Option<DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY>,
|
||||
}
|
||||
|
||||
impl From<MockDevice> for win32_display_data::Device {
|
||||
fn from(mock: MockDevice) -> Self {
|
||||
win32_display_data::Device {
|
||||
hmonitor: mock.hmonitor,
|
||||
device_path: mock.device_path,
|
||||
device_name: mock.device_name,
|
||||
device_description: mock.device_description,
|
||||
serial_number_id: mock.serial_number_id,
|
||||
size: mock.size,
|
||||
work_area_size: mock.work_area_size,
|
||||
device_key: mock.device_key,
|
||||
output_technology: mock.output_technology,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Creating a Window Manager Instance
|
||||
struct TestContext {
|
||||
socket_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Drop for TestContext {
|
||||
fn drop(&mut self) {
|
||||
if let Some(socket_path) = &self.socket_path {
|
||||
// Clean up the socket file
|
||||
if let Err(e) = std::fs::remove_file(socket_path) {
|
||||
tracing::warn!("Failed to remove socket file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_window_manager() -> (WindowManager, TestContext) {
|
||||
let (_sender, receiver): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
||||
bounded(1);
|
||||
|
||||
// Temporary socket path for testing
|
||||
let socket_name = format!("komorebi-test-{}.sock", Uuid::new_v4());
|
||||
let socket_path = PathBuf::from(socket_name);
|
||||
|
||||
// Create a new WindowManager instance
|
||||
let wm = match WindowManager::new(receiver, Some(socket_path.clone())) {
|
||||
Ok(manager) => manager,
|
||||
Err(e) => {
|
||||
panic!("Failed to create WindowManager: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
wm,
|
||||
TestContext {
|
||||
socket_path: Some(socket_path),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_notification() {
|
||||
// Create a monitor notification
|
||||
let notification = MonitorNotification::ResolutionScalingChanged;
|
||||
|
||||
// Use the send_notification function to send the notification
|
||||
send_notification(notification);
|
||||
|
||||
// Receive the notification from the channel
|
||||
let received = event_rx().try_recv();
|
||||
|
||||
// Check if we received the notification and if it matches what we sent
|
||||
match received {
|
||||
Ok(notification) => {
|
||||
assert_eq!(notification, MonitorNotification::ResolutionScalingChanged);
|
||||
}
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channel_bounded_capacity() {
|
||||
let (_, receiver) = channel();
|
||||
|
||||
// Fill the channel to its capacity (20 messages)
|
||||
for _ in 0..20 {
|
||||
send_notification(MonitorNotification::WorkAreaChanged);
|
||||
}
|
||||
|
||||
// Attempt to send another message (should be dropped)
|
||||
send_notification(MonitorNotification::ResolutionScalingChanged);
|
||||
|
||||
// Verify the channel contains only the first 20 messages
|
||||
for _ in 0..20 {
|
||||
let notification = match receiver.try_recv() {
|
||||
Ok(notification) => notification,
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
};
|
||||
assert_eq!(
|
||||
notification,
|
||||
MonitorNotification::WorkAreaChanged,
|
||||
"Unexpected notification in the channel"
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that no additional messages are in the channel
|
||||
assert!(
|
||||
receiver.try_recv().is_err(),
|
||||
"Channel should be empty after consuming all messages"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_in_monitor_cache() {
|
||||
let m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"Test Monitor".to_string(),
|
||||
"Test Device".to_string(),
|
||||
"Test Device ID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Insert the monitor into the cache
|
||||
insert_in_monitor_cache("TestMonitorID", m.clone());
|
||||
|
||||
// Retrieve the monitor from the cache
|
||||
let cache = MONITOR_CACHE
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock();
|
||||
let retrieved_monitor = cache.get("TestMonitorID");
|
||||
|
||||
// Check that the monitor was inserted correctly and matches the expected value
|
||||
assert_eq!(retrieved_monitor, Some(&m));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_two_monitors_cache() {
|
||||
let m1 = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"Test Monitor".to_string(),
|
||||
"Test Device".to_string(),
|
||||
"Test Device ID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
let m2 = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"Test Monitor 2".to_string(),
|
||||
"Test Device 2".to_string(),
|
||||
"Test Device ID 2".to_string(),
|
||||
Some("TestMonitorID2".to_string()),
|
||||
);
|
||||
|
||||
// Insert the first monitor into the cache
|
||||
insert_in_monitor_cache("TestMonitorID", m1.clone());
|
||||
|
||||
// Insert the second monitor into the cache
|
||||
insert_in_monitor_cache("TestMonitorID2", m2.clone());
|
||||
|
||||
// Retrieve the cache to check if the first and second monitors are present
|
||||
let cache = MONITOR_CACHE
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock();
|
||||
|
||||
// Check if Monitor 1 was found in the cache
|
||||
assert_eq!(
|
||||
cache.get("TestMonitorID"),
|
||||
Some(&m1),
|
||||
"Monitor cache should contain monitor 1"
|
||||
);
|
||||
|
||||
// Check if Monitor 2 was found in the cache
|
||||
assert_eq!(
|
||||
cache.get("TestMonitorID2"),
|
||||
Some(&m2),
|
||||
"Monitor cache should contain monitor 2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_listen_for_notifications() {
|
||||
// Create a WindowManager instance for testing
|
||||
let (wm, _test_context) = setup_window_manager();
|
||||
|
||||
// Start the notification listener
|
||||
let result = listen_for_notifications(Arc::new(Mutex::new(wm)));
|
||||
|
||||
// Check if the listener started successfully
|
||||
assert!(result.is_ok(), "Failed to start notification listener");
|
||||
|
||||
// Test sending a notification
|
||||
send_notification(MonitorNotification::DisplayConnectionChange);
|
||||
|
||||
// Receive the notification from the channel
|
||||
let received = event_rx().try_recv();
|
||||
|
||||
// Check if we received the notification and if it matches what we sent
|
||||
match received {
|
||||
Ok(notification) => {
|
||||
assert_eq!(notification, MonitorNotification::DisplayConnectionChange);
|
||||
}
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attached_display_devices() {
|
||||
// Define mock display data
|
||||
let mock_monitor = MockDevice {
|
||||
hmonitor: 1,
|
||||
device_path: String::from(
|
||||
"\\\\?\\DISPLAY#ABC123#4&123456&0&UID0#{saucepackets-4321-5678-2468-abc123456789}",
|
||||
),
|
||||
device_name: String::from("\\\\.\\DISPLAY1"),
|
||||
device_description: String::from("Display description"),
|
||||
serial_number_id: Some(String::from("SaucePackets123")),
|
||||
device_key: String::from("Mock Key"),
|
||||
size: RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 1920,
|
||||
bottom: 1080,
|
||||
},
|
||||
work_area_size: RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 1920,
|
||||
bottom: 1080,
|
||||
},
|
||||
output_technology: Some(DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY(0)),
|
||||
};
|
||||
|
||||
// Create a closure to simulate the display provider
|
||||
let display_provider = || {
|
||||
vec![Ok::<win32_display_data::Device, win32_display_data::Error>(
|
||||
win32_display_data::Device::from(mock_monitor.clone()),
|
||||
)]
|
||||
.into_iter()
|
||||
};
|
||||
|
||||
// Should contain the mock monitor
|
||||
let result = attached_display_devices(display_provider).ok();
|
||||
if let Some(monitors) = result {
|
||||
// Check Number of monitors
|
||||
assert_eq!(monitors.len(), 1, "Expected one monitor");
|
||||
|
||||
// hmonitor
|
||||
assert_eq!(monitors[0].id(), 1);
|
||||
|
||||
// device name
|
||||
assert_eq!(monitors[0].name(), &String::from("DISPLAY1"));
|
||||
|
||||
// Device
|
||||
assert_eq!(monitors[0].device(), &String::from("ABC123"));
|
||||
|
||||
// Device ID
|
||||
assert_eq!(
|
||||
monitors[0].device_id(),
|
||||
&String::from("ABC123-4&123456&0&UID0")
|
||||
);
|
||||
|
||||
// Check monitor serial number id
|
||||
assert_eq!(
|
||||
monitors[0].serial_number_id,
|
||||
Some(String::from("SaucePackets123")),
|
||||
);
|
||||
} else {
|
||||
panic!("No monitors found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::OptionExt;
|
||||
use color_eyre::Result;
|
||||
use komorebi_themes::colour::Rgb;
|
||||
use miow::pipe::connect;
|
||||
use net2::TcpStreamExt;
|
||||
use parking_lot::Mutex;
|
||||
@@ -18,9 +20,18 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use crate::animation::ANIMATION_DURATION_GLOBAL;
|
||||
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_FPS;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::IMPLEMENTATION;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::build;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
@@ -37,17 +48,9 @@ use crate::core::SocketMessage;
|
||||
use crate::core::StateQuery;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowKind;
|
||||
|
||||
use crate::animation::ANIMATION_DURATION_GLOBAL;
|
||||
use crate::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use crate::animation::ANIMATION_FPS;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::IMPLEMENTATION;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::colour::Rgb;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use crate::default_layout::ScrollingLayoutOptions;
|
||||
use crate::monitor::MonitorInformation;
|
||||
use crate::notify_subscribers;
|
||||
use crate::stackbar_manager;
|
||||
@@ -71,6 +74,7 @@ use crate::State;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
@@ -80,6 +84,7 @@ use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::SESSION_FLOATING_APPLICATIONS;
|
||||
use crate::SUBSCRIPTION_PIPES;
|
||||
use crate::SUBSCRIPTION_SOCKETS;
|
||||
use crate::SUBSCRIPTION_SOCKET_OPTIONS;
|
||||
@@ -223,6 +228,7 @@ impl WindowManager {
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut force_update_borders = false;
|
||||
match message {
|
||||
SocketMessage::Promote => self.promote_container_to_front()?,
|
||||
SocketMessage::PromoteFocus => self.promote_focus_to_front()?,
|
||||
@@ -232,29 +238,37 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::EagerFocus(ref exe) => {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx = self.focused_workspace_idx()?;
|
||||
|
||||
let mut window_location = None;
|
||||
let mut monitor_workspace_indices = None;
|
||||
let mut monitor_to_focus = None;
|
||||
let mut needs_workspace_loading = false;
|
||||
|
||||
'search: for (monitor_idx, monitor) in self.monitors().iter().enumerate() {
|
||||
'search: for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
||||
for (workspace_idx, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
if let Some(location) = workspace.location_from_exe(exe) {
|
||||
window_location = Some(location);
|
||||
monitor_workspace_indices = Some((monitor_idx, workspace_idx));
|
||||
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
monitor_to_focus = Some(monitor_idx);
|
||||
}
|
||||
|
||||
// Focus workspace if it is not already the focused one, without
|
||||
// loading it so that we don't give focus to the wrong window, we will
|
||||
// load it later after focusing the wanted window
|
||||
let focused_ws_idx = monitor.focused_workspace_idx();
|
||||
if focused_ws_idx != workspace_idx {
|
||||
monitor.set_last_focused_workspace(Option::from(focused_ws_idx));
|
||||
monitor.focus_workspace(workspace_idx)?;
|
||||
needs_workspace_loading = true;
|
||||
}
|
||||
|
||||
break 'search;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((monitor_idx, workspace_idx)) = monitor_workspace_indices {
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
|
||||
if workspace_idx != focused_workspace_idx {
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
if let Some(monitor_idx) = monitor_to_focus {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
|
||||
if let Some(location) = window_location {
|
||||
@@ -287,6 +301,13 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needs_workspace_loading {
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusWindow(direction) => {
|
||||
@@ -331,11 +352,9 @@ impl WindowManager {
|
||||
SocketMessage::UnstackAll => self.unstack_all()?,
|
||||
SocketMessage::CycleStack(direction) => {
|
||||
self.cycle_container_window_in_direction(direction)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
SocketMessage::CycleStackIndex(direction) => {
|
||||
self.cycle_container_window_index_in_direction(direction)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
SocketMessage::FocusStackWindow(idx) => {
|
||||
// In case you are using this command on a bar on a monitor
|
||||
@@ -346,7 +365,6 @@ impl WindowManager {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
self.focus_container_window(idx)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
SocketMessage::ForceFocus => {
|
||||
let focused_window = self.focused_window()?;
|
||||
@@ -360,7 +378,42 @@ impl WindowManager {
|
||||
SocketMessage::Minimize => {
|
||||
Window::from(WindowsApi::foreground_window()?).minimize();
|
||||
}
|
||||
SocketMessage::ToggleFloat => self.toggle_float()?,
|
||||
SocketMessage::LockMonitorWorkspaceContainer(
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
container_idx,
|
||||
) => {
|
||||
let monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(monitor_idx)
|
||||
.ok_or_eyre("no monitor at the given index")?;
|
||||
|
||||
let workspace = monitor
|
||||
.workspaces_mut()
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_eyre("no workspace at the given index")?;
|
||||
|
||||
workspace.locked_containers.insert(container_idx);
|
||||
}
|
||||
SocketMessage::UnlockMonitorWorkspaceContainer(
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
container_idx,
|
||||
) => {
|
||||
let monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(monitor_idx)
|
||||
.ok_or_eyre("no monitor at the given index")?;
|
||||
|
||||
let workspace = monitor
|
||||
.workspaces_mut()
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_eyre("no workspace at the given index")?;
|
||||
|
||||
workspace.locked_containers.remove(&container_idx);
|
||||
}
|
||||
SocketMessage::ToggleLock => self.toggle_lock()?,
|
||||
SocketMessage::ToggleFloat => self.toggle_float(false)?,
|
||||
SocketMessage::ToggleMonocle => self.toggle_monocle()?,
|
||||
SocketMessage::ToggleMaximize => self.toggle_maximize()?,
|
||||
SocketMessage::ContainerPadding(monitor_idx, workspace_idx, size) => {
|
||||
@@ -507,6 +560,53 @@ impl WindowManager {
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::SessionFloatRule => {
|
||||
let foreground_window = WindowsApi::foreground_window()?;
|
||||
let window = Window::from(foreground_window);
|
||||
if let (Ok(exe), Ok(title), Ok(class)) =
|
||||
(window.exe(), window.title(), window.class())
|
||||
{
|
||||
let rule = MatchingRule::Composite(vec![
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: exe,
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Title,
|
||||
id: title,
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: class,
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
]);
|
||||
|
||||
let mut floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
floating_applications.push(rule.clone());
|
||||
let mut session_floating_applications = SESSION_FLOATING_APPLICATIONS.lock();
|
||||
session_floating_applications.push(rule.clone());
|
||||
|
||||
self.toggle_float(true)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::SessionFloatRules => {
|
||||
let session_floating_applications = SESSION_FLOATING_APPLICATIONS.lock();
|
||||
let rules = match serde_json::to_string_pretty(&*session_floating_applications) {
|
||||
Ok(rules) => rules,
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
|
||||
reply.write_all(rules.as_bytes())?;
|
||||
}
|
||||
SocketMessage::ClearSessionFloatRules => {
|
||||
let mut floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
let mut session_floating_applications = SESSION_FLOATING_APPLICATIONS.lock();
|
||||
floating_applications.retain(|r| !session_floating_applications.contains(r));
|
||||
session_floating_applications.clear()
|
||||
}
|
||||
SocketMessage::IgnoreRule(identifier, ref id) => {
|
||||
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
|
||||
|
||||
@@ -605,6 +705,67 @@ impl WindowManager {
|
||||
SocketMessage::AdjustWorkspacePadding(sizing, adjustment) => {
|
||||
self.adjust_workspace_padding(sizing, adjustment)?;
|
||||
}
|
||||
SocketMessage::MoveContainerToLastWorkspace => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
if monitor_idx != self.focused_monitor_idx() {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if workspace.is_empty() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.focused_workspace_idx();
|
||||
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
if let Some(last_focused_workspace) = monitor.last_focused_workspace() {
|
||||
self.move_container_to_workspace(last_focused_workspace, true, None)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.set_last_focused_workspace(Option::from(idx));
|
||||
}
|
||||
SocketMessage::SendContainerToLastWorkspace => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
if monitor_idx != self.focused_monitor_idx() {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if workspace.is_empty() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.focused_workspace_idx();
|
||||
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
if let Some(last_focused_workspace) = monitor.last_focused_workspace() {
|
||||
self.move_container_to_workspace(last_focused_workspace, false, None)?;
|
||||
}
|
||||
}
|
||||
self.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.set_last_focused_workspace(Option::from(idx));
|
||||
}
|
||||
SocketMessage::MoveContainerToWorkspaceNumber(workspace_idx) => {
|
||||
self.move_container_to_workspace(workspace_idx, true, None)?;
|
||||
}
|
||||
@@ -765,13 +926,36 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::Retile => {
|
||||
border_manager::destroy_all_borders()?;
|
||||
force_update_borders = true;
|
||||
self.retile_all(false)?
|
||||
}
|
||||
SocketMessage::RetileWithResizeDimensions => {
|
||||
border_manager::destroy_all_borders()?;
|
||||
force_update_borders = true;
|
||||
self.retile_all(true)?
|
||||
}
|
||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
||||
SocketMessage::ScrollingLayoutColumns(count) => {
|
||||
let focused_workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let options = match focused_workspace.layout_options() {
|
||||
Some(mut opts) => {
|
||||
if let Some(scrolling) = &mut opts.scrolling {
|
||||
scrolling.columns = count.into();
|
||||
}
|
||||
|
||||
opts
|
||||
}
|
||||
None => LayoutOptions {
|
||||
scrolling: Some(ScrollingLayoutOptions {
|
||||
columns: count.into(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
focused_workspace.set_layout_options(Some(options));
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
||||
SocketMessage::ChangeLayoutCustom(ref path) => {
|
||||
@@ -1101,11 +1285,35 @@ impl WindowManager {
|
||||
WorkspaceLayer::Tiling => {
|
||||
workspace.set_layer(WorkspaceLayer::Floating);
|
||||
|
||||
for (i, window) in workspace.floating_windows().iter().enumerate() {
|
||||
if i == 0 {
|
||||
let focused_idx = workspace.focused_floating_window_idx();
|
||||
let mut window_idx_pairs = workspace
|
||||
.floating_windows_mut()
|
||||
.make_contiguous()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort by window area
|
||||
window_idx_pairs.sort_by_key(|(_, w)| {
|
||||
let rect = WindowsApi::window_rect(w.hwnd).unwrap_or_default();
|
||||
rect.right * rect.bottom
|
||||
});
|
||||
window_idx_pairs.reverse();
|
||||
|
||||
for (i, window) in window_idx_pairs {
|
||||
if i == focused_idx {
|
||||
to_focus = Some(*window);
|
||||
} else {
|
||||
window.restore();
|
||||
window.raise()?;
|
||||
}
|
||||
window.raise()?;
|
||||
}
|
||||
|
||||
if let Some(focused_window) = &to_focus {
|
||||
// The focused window should be the last one raised to make sure it is
|
||||
// on top
|
||||
focused_window.restore();
|
||||
focused_window.raise()?;
|
||||
}
|
||||
|
||||
for container in workspace.containers() {
|
||||
@@ -1113,22 +1321,51 @@ impl WindowManager {
|
||||
window.lower()?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
window.lower()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
WorkspaceLayer::Floating => {
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
for (i, container) in workspace.containers_mut().iter_mut().enumerate() {
|
||||
if let Some(window) = container.focused_window() {
|
||||
if i == focused_container_idx {
|
||||
to_focus = Some(*window);
|
||||
}
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
to_focus = Some(*window);
|
||||
window.raise()?;
|
||||
}
|
||||
}
|
||||
for window in workspace.floating_windows() {
|
||||
window.hide();
|
||||
}
|
||||
} else {
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
for (i, container) in workspace.containers_mut().iter_mut().enumerate()
|
||||
{
|
||||
if let Some(window) = container.focused_window() {
|
||||
if i == focused_container_idx {
|
||||
to_focus = Some(*window);
|
||||
}
|
||||
window.raise()?;
|
||||
}
|
||||
}
|
||||
|
||||
for window in workspace.floating_windows() {
|
||||
window.lower()?;
|
||||
let mut window_idx_pairs = workspace
|
||||
.floating_windows_mut()
|
||||
.make_contiguous()
|
||||
.iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort by window area
|
||||
window_idx_pairs.sort_by_key(|w| {
|
||||
let rect = WindowsApi::window_rect(w.hwnd).unwrap_or_default();
|
||||
rect.right * rect.bottom
|
||||
});
|
||||
|
||||
for window in window_idx_pairs {
|
||||
window.lower()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1155,7 +1392,7 @@ impl WindowManager {
|
||||
);
|
||||
}
|
||||
SocketMessage::DisplayIndexPreference(index_preference, ref display) => {
|
||||
let mut display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
|
||||
let mut display_index_preferences = DISPLAY_INDEX_PREFERENCES.write();
|
||||
display_index_preferences.insert(index_preference, display.clone());
|
||||
}
|
||||
SocketMessage::EnsureWorkspaces(monitor_idx, workspace_count) => {
|
||||
@@ -1247,6 +1484,32 @@ impl WindowManager {
|
||||
.focused_workspace_name()
|
||||
.unwrap_or_else(|| focused_monitor.focused_workspace_idx().to_string())
|
||||
}
|
||||
StateQuery::Version => build::RUST_VERSION.to_string(),
|
||||
StateQuery::FocusedWorkspaceLayout => {
|
||||
let focused_monitor = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
focused_monitor.focused_workspace_layout().map_or_else(
|
||||
|| "None".to_string(),
|
||||
|layout| match layout {
|
||||
Layout::Default(default_layout) => default_layout.to_string(),
|
||||
Layout::Custom(_) => "Custom".to_string(),
|
||||
},
|
||||
)
|
||||
}
|
||||
StateQuery::FocusedContainerKind => {
|
||||
match self.focused_workspace()?.focused_container() {
|
||||
None => "None".to_string(),
|
||||
Some(container) => {
|
||||
if container.windows().len() > 1 {
|
||||
"Stack".to_string()
|
||||
} else {
|
||||
"Single".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
reply.write_all(response.as_bytes())?;
|
||||
@@ -1456,6 +1719,7 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::ReloadConfiguration => {
|
||||
Self::reload_configuration();
|
||||
force_update_borders = true;
|
||||
}
|
||||
SocketMessage::ReplaceConfiguration(ref config) => {
|
||||
// Check that this is a valid static config file first
|
||||
@@ -1484,15 +1748,78 @@ impl WindowManager {
|
||||
|
||||
// Set self to the new wm instance
|
||||
*self = wm;
|
||||
|
||||
// check if there are any bars
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
|
||||
|
||||
let has_bar = system
|
||||
.processes_by_name("komorebi-bar.exe".as_ref())
|
||||
.next()
|
||||
.is_some();
|
||||
|
||||
// stop bar(s)
|
||||
if has_bar {
|
||||
let script = r"
|
||||
Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
|
||||
// start new bar(s)
|
||||
let mut config = StaticConfig::read(config)?;
|
||||
if let Some(display_bar_configurations) =
|
||||
&mut config.bar_configurations
|
||||
{
|
||||
for config_file_path in &mut *display_bar_configurations {
|
||||
let script = r#"Start-Process "komorebi-bar" '"--config" "CONFIGFILE"' -WindowStyle hidden"#
|
||||
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
|
||||
|
||||
match powershell_script::run(&script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let script = r"
|
||||
if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
|
||||
{
|
||||
Start-Process komorebi-bar -WindowStyle hidden
|
||||
}
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
force_update_borders = true;
|
||||
}
|
||||
}
|
||||
SocketMessage::ReloadStaticConfiguration(ref pathbuf) => {
|
||||
self.reload_static_configuration(pathbuf)?;
|
||||
force_update_borders = true;
|
||||
}
|
||||
SocketMessage::CompleteConfiguration => {
|
||||
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
||||
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
force_update_borders = true;
|
||||
}
|
||||
}
|
||||
SocketMessage::WatchConfiguration(enable) => {
|
||||
@@ -1750,6 +2077,8 @@ impl WindowManager {
|
||||
self.remove_all_accents()?;
|
||||
}
|
||||
}
|
||||
} else if matches!(IMPLEMENTATION.load(), BorderImplementation::Komorebi) {
|
||||
force_update_borders = true;
|
||||
}
|
||||
}
|
||||
SocketMessage::BorderImplementation(implementation) => {
|
||||
@@ -1762,40 +2091,49 @@ impl WindowManager {
|
||||
match IMPLEMENTATION.load() {
|
||||
BorderImplementation::Komorebi => {
|
||||
self.remove_all_accents()?;
|
||||
force_update_borders = true;
|
||||
}
|
||||
BorderImplementation::Windows => {
|
||||
border_manager::destroy_all_borders()?;
|
||||
}
|
||||
}
|
||||
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
}
|
||||
SocketMessage::BorderColour(kind, r, g, b) => match kind {
|
||||
WindowKind::Single => {
|
||||
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
SocketMessage::BorderColour(kind, r, g, b) => {
|
||||
match kind {
|
||||
WindowKind::Single => {
|
||||
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Stack => {
|
||||
border_manager::STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Monocle => {
|
||||
border_manager::MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Unfocused => {
|
||||
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::UnfocusedLocked => {
|
||||
border_manager::UNFOCUSED_LOCKED
|
||||
.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Floating => {
|
||||
border_manager::FLOATING.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
WindowKind::Stack => {
|
||||
border_manager::STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Monocle => {
|
||||
border_manager::MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Unfocused => {
|
||||
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Floating => {
|
||||
border_manager::FLOATING.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
},
|
||||
force_update_borders = true;
|
||||
}
|
||||
SocketMessage::BorderStyle(style) => {
|
||||
STYLE.store(style);
|
||||
force_update_borders = true;
|
||||
}
|
||||
SocketMessage::BorderWidth(width) => {
|
||||
border_manager::BORDER_WIDTH.store(width, Ordering::SeqCst);
|
||||
force_update_borders = true;
|
||||
}
|
||||
SocketMessage::BorderOffset(offset) => {
|
||||
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
|
||||
force_update_borders = true;
|
||||
}
|
||||
SocketMessage::Animation(enable, prefix) => match prefix {
|
||||
Some(prefix) => {
|
||||
@@ -1957,8 +2295,8 @@ impl WindowManager {
|
||||
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
theme_manager::send_notification(theme);
|
||||
SocketMessage::Theme(ref theme) => {
|
||||
theme_manager::send_notification(*theme.clone());
|
||||
}
|
||||
// Deprecated commands
|
||||
SocketMessage::AltFocusHack(_)
|
||||
@@ -1976,7 +2314,11 @@ impl WindowManager {
|
||||
initial_state.has_been_modified(self.as_ref()),
|
||||
)?;
|
||||
|
||||
border_manager::send_notification(None);
|
||||
if force_update_borders {
|
||||
border_manager::send_force_update();
|
||||
} else {
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
transparency_manager::send_notification();
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
@@ -2077,20 +2419,19 @@ mod tests {
|
||||
use crate::Rect;
|
||||
use crate::SocketMessage;
|
||||
use crate::WindowManagerEvent;
|
||||
use crate::DATA_DIR;
|
||||
use crossbeam_channel::bounded;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use uds_windows::UnixStream;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn send_socket_message(socket: &str, message: SocketMessage) {
|
||||
let socket = DATA_DIR.join(socket);
|
||||
fn send_socket_message(socket: &PathBuf, message: SocketMessage) {
|
||||
let mut stream = UnixStream::connect(socket).unwrap();
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_secs(1)))
|
||||
@@ -2105,7 +2446,7 @@ mod tests {
|
||||
let (_sender, receiver): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
||||
bounded(1);
|
||||
let socket_name = format!("komorebi-test-{}.sock", Uuid::new_v4());
|
||||
let socket_path = DATA_DIR.join(&socket_name);
|
||||
let socket_path = PathBuf::from(&socket_name);
|
||||
let mut wm = WindowManager::new(receiver, Some(socket_path.clone())).unwrap();
|
||||
let m = monitor::new(
|
||||
0,
|
||||
@@ -2120,7 +2461,7 @@ mod tests {
|
||||
wm.monitors_mut().push_back(m);
|
||||
|
||||
// send a message
|
||||
send_socket_message(&socket_name, SocketMessage::FocusWorkspaceNumber(5));
|
||||
send_socket_message(&socket_path, SocketMessage::FocusWorkspaceNumber(5));
|
||||
|
||||
let (stream, _) = wm.command_listener.accept().unwrap();
|
||||
let reader = BufReader::new(stream.try_clone().unwrap());
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
@@ -27,12 +25,14 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::workspace_reconciliator;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Layout;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::VirtualDesktopNotification;
|
||||
use crate::Window;
|
||||
use crate::CURRENT_VIRTUAL_DESKTOP;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
@@ -120,15 +120,62 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
let mut last_known_virtual_desktop_id = CURRENT_VIRTUAL_DESKTOP.lock();
|
||||
|
||||
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
|
||||
if let Some(id) = current_virtual_desktop() {
|
||||
let latest_virtual_desktop_id = current_virtual_desktop();
|
||||
if let Some(id) = latest_virtual_desktop_id {
|
||||
// if we are on the vd associated with komorebi
|
||||
let should_retile = id == *virtual_desktop_id
|
||||
// and we came from a vd not associated with komorebi
|
||||
&& (*last_known_virtual_desktop_id).clone().unwrap_or_default() != id;
|
||||
|
||||
*last_known_virtual_desktop_id = Some(id.clone());
|
||||
if id != *virtual_desktop_id {
|
||||
tracing::info!(
|
||||
"ignoring events and commands while not on virtual desktop {:?}",
|
||||
virtual_desktop_id
|
||||
);
|
||||
|
||||
// TODO: when returning from another VD to the VD associated with komorebi
|
||||
// if borders are enabled, they will not be drawn again until the user interacts
|
||||
// with the workspace or forces a retile
|
||||
border_manager::destroy_all_borders()?;
|
||||
|
||||
// to be consumed by integrating gui applications like bars to know
|
||||
// when to hide visual components which don't make sense when not on
|
||||
// komorebi's associated virtual desktop
|
||||
tracing::debug!("notifying subscribers that we have left komorebi's associated virtual desktop");
|
||||
notify_subscribers(
|
||||
Notification {
|
||||
event: NotificationEvent::VirtualDesktop(
|
||||
VirtualDesktopNotification::LeftAssociatedVirtualDesktop,
|
||||
),
|
||||
state: self.as_ref().into(),
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if should_retile {
|
||||
self.retile_all(true)?;
|
||||
|
||||
// to be consumed by integrating gui applications like bars to know
|
||||
// when to show visual components associated with komorebi's virtual
|
||||
// desktop
|
||||
tracing::debug!("notifying subscribers that we are back on komorebi's associated virtual desktop");
|
||||
notify_subscribers(
|
||||
Notification {
|
||||
event: NotificationEvent::VirtualDesktop(
|
||||
VirtualDesktopNotification::EnteredAssociatedVirtualDesktop,
|
||||
),
|
||||
state: self.as_ref().into(),
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +303,11 @@ impl WindowManager {
|
||||
// don't want to trigger the full workspace updates when there are no managed
|
||||
// containers - this makes floating windows on empty workspaces go into very
|
||||
// annoying focus change loops which prevents users from interacting with them
|
||||
if !self.focused_workspace()?.containers().is_empty() {
|
||||
if !matches!(
|
||||
self.focused_workspace()?.layout(),
|
||||
Layout::Default(DefaultLayout::Scrolling)
|
||||
) && !self.focused_workspace()?.containers().is_empty()
|
||||
{
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
}
|
||||
|
||||
@@ -283,6 +334,14 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
|
||||
if matches!(
|
||||
self.focused_workspace()?.layout(),
|
||||
Layout::Default(DefaultLayout::Scrolling)
|
||||
) && !self.focused_workspace()?.containers().is_empty()
|
||||
{
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
}
|
||||
}
|
||||
Some(idx) => {
|
||||
if let Some(_window) = workspace.floating_windows().get(idx) {
|
||||
@@ -304,34 +363,7 @@ impl WindowManager {
|
||||
let focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
let mut needs_reconciliation = false;
|
||||
|
||||
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if focused_pair != (*m_idx, *w_idx) {
|
||||
// At this point we know we are going to send a notification to the workspace reconciliator
|
||||
// So we get the topmost window returned by EnumWindows, which is almost always the window
|
||||
// that has been selected by alt-tab
|
||||
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
|
||||
if let Some(first) =
|
||||
alt_tab_windows.iter().find(|w| w.title().is_ok())
|
||||
{
|
||||
// If our record of this HWND hasn't been updated in over a minute
|
||||
let mut instant = ALT_TAB_HWND_INSTANT.lock();
|
||||
if instant.elapsed().gt(&Duration::from_secs(1)) {
|
||||
// Update our record with the HWND we just found
|
||||
ALT_TAB_HWND.store(Some(first.hwnd));
|
||||
// Update the timestamp of our record
|
||||
*instant = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace_reconciliator::send_notification(*m_idx, *w_idx);
|
||||
needs_reconciliation = true;
|
||||
}
|
||||
}
|
||||
let mut needs_reconciliation = None;
|
||||
|
||||
// There are some applications such as Firefox where, if they are focused when a
|
||||
// workspace switch takes place, it will fire an additional Show event, which will
|
||||
@@ -340,6 +372,23 @@ impl WindowManager {
|
||||
// duplicates across multiple workspaces, as it results in ghost layout tiles.
|
||||
let mut proceed = true;
|
||||
|
||||
// Check for potential `alt-tab` event
|
||||
if matches!(
|
||||
event,
|
||||
WindowManagerEvent::Uncloak(_, _) | WindowManagerEvent::Show(_, _)
|
||||
) {
|
||||
needs_reconciliation = self.needs_reconciliation(window)?;
|
||||
|
||||
if let Some((m_idx, ws_idx)) = needs_reconciliation {
|
||||
self.perform_reconciliation(window, (m_idx, ws_idx))?;
|
||||
|
||||
// Since there was a reconciliation after an `alt-tab`, that means this
|
||||
// window is already handled by komorebi so we shouldn't proceed with
|
||||
// adding it as a new window.
|
||||
proceed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if let Some(focused_workspace_idx) = self
|
||||
.monitors()
|
||||
@@ -360,7 +409,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
if proceed {
|
||||
let mut behaviour = self.window_management_behaviour(
|
||||
let behaviour = self.window_management_behaviour(
|
||||
focused_monitor_idx,
|
||||
focused_workspace_idx,
|
||||
);
|
||||
@@ -368,7 +417,7 @@ impl WindowManager {
|
||||
let workspace_contains_window = workspace.contains_window(window.hwnd);
|
||||
let monocle_container = workspace.monocle_container().clone();
|
||||
|
||||
if !workspace_contains_window && !needs_reconciliation {
|
||||
if !workspace_contains_window && needs_reconciliation.is_none() {
|
||||
let floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
let mut should_float = false;
|
||||
|
||||
@@ -390,13 +439,33 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
behaviour.float_override = behaviour.float_override
|
||||
|| (should_float
|
||||
&& !matches!(event, WindowManagerEvent::Manage(_)));
|
||||
|
||||
if behaviour.float_override {
|
||||
workspace.floating_windows_mut().push(window);
|
||||
if behaviour.float_override
|
||||
|| behaviour.floating_layer_override
|
||||
|| (should_float && !matches!(event, WindowManagerEvent::Manage(_)))
|
||||
{
|
||||
let placement = if behaviour.floating_layer_override {
|
||||
// Floating layer override placement
|
||||
behaviour.floating_layer_placement
|
||||
} else if behaviour.float_override {
|
||||
// Float override placement
|
||||
behaviour.float_override_placement
|
||||
} else {
|
||||
// Float rule placement
|
||||
behaviour.float_rule_placement
|
||||
};
|
||||
// Center floating windows according to the proper placement if not
|
||||
// on a floating workspace
|
||||
let center_spawned_floats =
|
||||
placement.should_center() && workspace.tile;
|
||||
workspace.floating_windows_mut().push_back(window);
|
||||
workspace.set_layer(WorkspaceLayer::Floating);
|
||||
if center_spawned_floats {
|
||||
let mut floating_window = window;
|
||||
floating_window.center(
|
||||
&workspace.globals().work_area,
|
||||
placement.should_resize(),
|
||||
)?;
|
||||
}
|
||||
self.update_focused_workspace(false, false)?;
|
||||
} else {
|
||||
match behaviour.current_behaviour {
|
||||
@@ -445,7 +514,11 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
if !monocle_window_event && monocle_container.is_some() {
|
||||
let workspace = self.focused_workspace()?;
|
||||
if !(monocle_window_event
|
||||
|| workspace.layer() != &WorkspaceLayer::Tiling)
|
||||
&& monocle_container.is_some()
|
||||
{
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
@@ -619,7 +692,7 @@ impl WindowManager {
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
} else if window_management_behaviour.float_override {
|
||||
workspace.floating_windows_mut().push(window);
|
||||
workspace.floating_windows_mut().push_back(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
} else {
|
||||
match window_management_behaviour.current_behaviour {
|
||||
@@ -712,7 +785,7 @@ impl WindowManager {
|
||||
|
||||
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
|
||||
if let WindowManagerEvent::Unmanage(mut window) = event {
|
||||
window.center(&self.focused_monitor_work_area()?)?;
|
||||
window.center(&self.focused_monitor_work_area()?, true)?;
|
||||
}
|
||||
|
||||
// Update list of known_hwnds and their monitor/workspace index pair
|
||||
@@ -742,4 +815,119 @@ impl WindowManager {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if this window is from another unfocused workspace or is an unfocused window on a
|
||||
/// stack container. If it is it will return the monitor/workspace index pair of this window so
|
||||
/// that a reconciliation of that monitor/workspace can be done.
|
||||
fn needs_reconciliation(&self, window: Window) -> color_eyre::Result<Option<(usize, usize)>> {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
let mut needs_reconciliation = None;
|
||||
|
||||
if let Some((m_idx, ws_idx)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if (*m_idx, *ws_idx) == focused_pair {
|
||||
if let Some(target_workspace) = self
|
||||
.monitors()
|
||||
.get(*m_idx)
|
||||
.and_then(|m| m.workspaces().get(*ws_idx))
|
||||
{
|
||||
if let Some(monocle_with_window) = target_workspace
|
||||
.monocle_container()
|
||||
.as_ref()
|
||||
.and_then(|m| m.contains_window(window.hwnd).then_some(m))
|
||||
{
|
||||
if monocle_with_window.focused_window() != Some(&window) {
|
||||
tracing::debug!("Needs reconciliation within a monocled stack");
|
||||
needs_reconciliation = Some((*m_idx, *ws_idx));
|
||||
}
|
||||
} else {
|
||||
let c_idx = target_workspace.container_idx_for_window(window.hwnd);
|
||||
|
||||
if let Some(target_container) =
|
||||
c_idx.and_then(|c_idx| target_workspace.containers().get(c_idx))
|
||||
{
|
||||
if target_container.focused_window() != Some(&window) {
|
||||
tracing::debug!(
|
||||
"Needs reconciliation within a stack on the focused workspace"
|
||||
);
|
||||
needs_reconciliation = Some((*m_idx, *ws_idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("Needs reconciliation for a different monitor/workspace pair");
|
||||
needs_reconciliation = Some((*m_idx, *ws_idx));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(needs_reconciliation)
|
||||
}
|
||||
|
||||
/// When there was an `alt-tab` to a hidden window we need to perform a reconciliation, meaning
|
||||
/// we need to update the focused monitor, workspace, container and window indices to the ones
|
||||
/// corresponding to the window the user just alt-tabbed into.
|
||||
fn perform_reconciliation(
|
||||
&mut self,
|
||||
window: Window,
|
||||
reconciliation_pair: (usize, usize),
|
||||
) -> color_eyre::Result<()> {
|
||||
let (m_idx, ws_idx) = reconciliation_pair;
|
||||
|
||||
tracing::debug!("performing reconciliation");
|
||||
self.focus_monitor(m_idx)?;
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
if ws_idx != monitor.focused_workspace_idx() {
|
||||
let previous_idx = monitor.focused_workspace_idx();
|
||||
monitor.set_last_focused_workspace(Option::from(previous_idx));
|
||||
monitor.focus_workspace(ws_idx)?;
|
||||
}
|
||||
if let Some(workspace) = monitor.focused_workspace_mut() {
|
||||
let mut layer = WorkspaceLayer::Tiling;
|
||||
if let Some((monocle, idx)) = workspace
|
||||
.monocle_container_mut()
|
||||
.as_mut()
|
||||
.and_then(|m| m.idx_for_window(window.hwnd).map(|i| (m, i)))
|
||||
{
|
||||
monocle.focus_window(idx);
|
||||
} else if workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.any(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
layer = WorkspaceLayer::Floating;
|
||||
} else if !workspace
|
||||
.maximized_window()
|
||||
.is_some_and(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
// If the window is the maximized window do nothing, else we
|
||||
// reintegrate the monocle if it exists and then focus the
|
||||
// container
|
||||
if workspace.monocle_container().is_some() {
|
||||
tracing::info!("disabling monocle");
|
||||
for container in workspace.containers_mut() {
|
||||
container.restore();
|
||||
}
|
||||
for window in workspace.floating_windows_mut() {
|
||||
window.restore();
|
||||
}
|
||||
workspace.reintegrate_monocle_container()?;
|
||||
}
|
||||
workspace.focus_container_by_window(window.hwnd)?;
|
||||
}
|
||||
workspace.set_layer(layer);
|
||||
}
|
||||
monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
use crate::border_manager;
|
||||
use crate::notify_subscribers;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::HidingBehaviour;
|
||||
use crate::NotificationEvent;
|
||||
use crate::Window;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowManagerEvent;
|
||||
use crate::DATA_DIR;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
@@ -169,6 +171,7 @@ fn find_orphans() -> color_eyre::Result<()> {
|
||||
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
let hiding_behaviour = *HIDING_BEHAVIOUR.lock();
|
||||
|
||||
let mut cache = HWNDS_CACHE.lock();
|
||||
let mut orphan_hwnds = HashMap::new();
|
||||
@@ -177,13 +180,18 @@ fn find_orphans() -> color_eyre::Result<()> {
|
||||
let window = Window::from(*hwnd);
|
||||
|
||||
if !window.is_window()
|
||||
// This one is a hack because WINWORD.EXE is an absolute trainwreck of an app
|
||||
// when multiple docs are open, it keeps open an invisible window, with WS_EX_LAYERED
|
||||
// (A STYLE THAT THE REGULAR WINDOWS NEED IN ORDER TO BE MANAGED!) when one of the
|
||||
// docs is closed
|
||||
//
|
||||
// I hate every single person who worked on Microsoft Office 365, especially Word
|
||||
|| !window.is_visible()
|
||||
|| (
|
||||
// This one is a hack because WINWORD.EXE is an absolute trainwreck of an app
|
||||
// when multiple docs are open, it keeps open an invisible window, with WS_EX_LAYERED
|
||||
// (A STYLE THAT THE REGULAR WINDOWS NEED IN ORDER TO BE MANAGED!) when one of the
|
||||
// docs is closed
|
||||
//
|
||||
// I hate every single person who worked on Microsoft Office 365, especially Word
|
||||
!window.is_visible()
|
||||
// We cannot execute this lovely hack if the user is using HidingBehaviour::Hide because
|
||||
// it will result in legitimate hidden, non-visible windows being yeeted from the state
|
||||
&& !matches!(hiding_behaviour, HidingBehaviour::Hide)
|
||||
)
|
||||
{
|
||||
orphan_hwnds.insert(window.hwnd, (*m_idx, *w_idx));
|
||||
}
|
||||
|
||||
@@ -76,4 +76,36 @@ macro_rules! impl_ring_elements {
|
||||
}
|
||||
}
|
||||
};
|
||||
// This allows passing a different name to be used for the functions. For instance, the
|
||||
// `floating_windows` ring calls this as:
|
||||
// ```rust
|
||||
// impl_ring_elements!(Workspace, Window, "floating_window");
|
||||
// ```
|
||||
// Which allows using the `Window` element but name the functions as `floating_window`
|
||||
($name:ty, $element:ident, $el_name:literal) => {
|
||||
paste::paste! {
|
||||
impl $name {
|
||||
pub const fn [<$el_name:lower s>](&self) -> &VecDeque<$element> {
|
||||
self.[<$el_name:lower s>].elements()
|
||||
}
|
||||
|
||||
pub fn [<$el_name:lower s_mut>](&mut self) -> &mut VecDeque<$element> {
|
||||
self.[<$el_name:lower s>].elements_mut()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn [<focused_ $el_name:lower>](&self) -> Option<&$element> {
|
||||
self.[<$el_name:lower s>].focused()
|
||||
}
|
||||
|
||||
pub const fn [<focused_ $el_name:lower _idx>](&self) -> usize {
|
||||
self.[<$el_name:lower s>].focused_idx()
|
||||
}
|
||||
|
||||
pub fn [<focused_ $el_name:lower _mut>](&mut self) -> Option<&mut $element> {
|
||||
self.[<$el_name:lower s>].focused_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443);
|
||||
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
|
||||
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
|
||||
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
|
||||
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::OnStack);
|
||||
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::Never);
|
||||
|
||||
pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
|
||||
@@ -180,7 +180,9 @@ impl Stackbar {
|
||||
layout.top -= workspace_specific_offset + STACKBAR_TAB_HEIGHT.load_consume();
|
||||
layout.left -= workspace_specific_offset;
|
||||
|
||||
WindowsApi::position_window(self.hwnd, &layout, false)?;
|
||||
// Async causes the stackbar to disappear or flicker because we modify it right after,
|
||||
// so we have to do a synchronous call
|
||||
WindowsApi::position_window(self.hwnd, &layout, false, false)?;
|
||||
|
||||
unsafe {
|
||||
let hdc = GetDC(Option::from(self.hwnd()));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,12 @@ use crate::stackbar_manager;
|
||||
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::Colour;
|
||||
use crate::KomorebiTheme;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use komorebi_themes::colour::Colour;
|
||||
use komorebi_themes::Base16Wrapper;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::OnceLock;
|
||||
@@ -76,6 +77,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
unfocused_locked_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
@@ -87,6 +89,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
unfocused_locked_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
@@ -112,6 +115,10 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let unfocused_locked_border = unfocused_locked_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Red)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
|
||||
.color32(name.as_theme());
|
||||
@@ -130,6 +137,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
unfocused_locked_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
@@ -142,6 +150,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
unfocused_locked_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
@@ -149,35 +158,39 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
} => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0D)
|
||||
.color32(*name);
|
||||
.color32(Base16Wrapper::Base16(*name));
|
||||
|
||||
let stack_border = stack_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(*name);
|
||||
.color32(Base16Wrapper::Base16(*name));
|
||||
|
||||
let monocle_border = monocle_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0F)
|
||||
.color32(*name);
|
||||
.color32(Base16Wrapper::Base16(*name));
|
||||
|
||||
let unfocused_border = unfocused_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(*name);
|
||||
.color32(Base16Wrapper::Base16(*name));
|
||||
|
||||
let unfocused_locked_border = unfocused_locked_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base08)
|
||||
.color32(Base16Wrapper::Base16(*name));
|
||||
|
||||
let floating_border = floating_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base09)
|
||||
.color32(*name);
|
||||
.color32(Base16Wrapper::Base16(*name));
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(*name);
|
||||
.color32(Base16Wrapper::Base16(*name));
|
||||
|
||||
let stackbar_unfocused_text = stackbar_unfocused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base05)
|
||||
.color32(*name);
|
||||
.color32(Base16Wrapper::Base16(*name));
|
||||
|
||||
let stackbar_background = stackbar_background
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(*name);
|
||||
.color32(Base16Wrapper::Base16(*name));
|
||||
|
||||
(
|
||||
single_border,
|
||||
@@ -185,6 +198,68 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
unfocused_locked_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
KomorebiTheme::Custom {
|
||||
colours,
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
unfocused_locked_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0D)
|
||||
.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
let stack_border = stack_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
let monocle_border = monocle_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0F)
|
||||
.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
let unfocused_border = unfocused_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
let unfocused_locked_border = unfocused_locked_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base08)
|
||||
.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
let floating_border = floating_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base09)
|
||||
.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
let stackbar_unfocused_text = stackbar_unfocused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base05)
|
||||
.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
let stackbar_background = stackbar_background
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
(
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
unfocused_locked_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
@@ -198,6 +273,10 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
border_manager::FLOATING.store(u32::from(Colour::from(floating_border)), Ordering::SeqCst);
|
||||
border_manager::UNFOCUSED
|
||||
.store(u32::from(Colour::from(unfocused_border)), Ordering::SeqCst);
|
||||
border_manager::UNFOCUSED_LOCKED.store(
|
||||
u32::from(Colour::from(unfocused_locked_border)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_background)),
|
||||
@@ -216,7 +295,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
|
||||
CURRENT_THEME.store(Some(notification.0));
|
||||
|
||||
border_manager::send_notification(None);
|
||||
border_manager::send_force_update();
|
||||
stackbar_manager::send_notification();
|
||||
}
|
||||
|
||||
|
||||
@@ -203,8 +203,8 @@ impl RenderDispatcher for MovementRenderDispatcher {
|
||||
fn render(&self, progress: f64) -> Result<()> {
|
||||
let new_rect = self.start_rect.lerp(self.target_rect, progress, self.style);
|
||||
|
||||
// using MoveWindow because it runs faster than SetWindowPos
|
||||
// so animation have more fps and feel smoother
|
||||
// we don't check WINDOW_HANDLING_BEHAVIOUR here because animations
|
||||
// are always run on a separate thread
|
||||
WindowsApi::move_window(self.hwnd, &new_rect, false)?;
|
||||
WindowsApi::invalidate_rect(self.hwnd, None, false);
|
||||
|
||||
@@ -212,7 +212,9 @@ impl RenderDispatcher for MovementRenderDispatcher {
|
||||
}
|
||||
|
||||
fn post_render(&self) -> Result<()> {
|
||||
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top)?;
|
||||
// we don't add the async_window_pos flag here because animations
|
||||
// are always run on a separate thread
|
||||
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false)?;
|
||||
if ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX)
|
||||
@@ -411,12 +413,18 @@ impl Window {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
|
||||
let (aspect_ratio_width, aspect_ratio_height) = FLOATING_WINDOW_TOGGLE_ASPECT_RATIO
|
||||
.lock()
|
||||
.width_and_height();
|
||||
let target_height = work_area.bottom / 2;
|
||||
let target_width = (target_height * aspect_ratio_width) / aspect_ratio_height;
|
||||
pub fn center(&mut self, work_area: &Rect, resize: bool) -> Result<()> {
|
||||
let (target_width, target_height) = if resize {
|
||||
let (aspect_ratio_width, aspect_ratio_height) = FLOATING_WINDOW_TOGGLE_ASPECT_RATIO
|
||||
.lock()
|
||||
.width_and_height();
|
||||
let target_height = work_area.bottom / 2;
|
||||
let target_width = (target_height * aspect_ratio_width) / aspect_ratio_height;
|
||||
(target_width, target_height)
|
||||
} else {
|
||||
let current_rect = WindowsApi::window_rect(self.hwnd)?;
|
||||
(current_rect.right, current_rect.bottom)
|
||||
};
|
||||
|
||||
let x = work_area.left + ((work_area.right - target_width) / 2);
|
||||
let y = work_area.top + ((work_area.bottom - target_height) / 2);
|
||||
@@ -461,7 +469,7 @@ impl Window {
|
||||
|
||||
AnimationEngine::animate(render_dispatcher, duration)
|
||||
} else {
|
||||
WindowsApi::position_window(self.hwnd, layout, top)
|
||||
WindowsApi::position_window(self.hwnd, layout, top, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::eyre::Error;
|
||||
use color_eyre::Result;
|
||||
use core::ffi::c_void;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::convert::TryFrom;
|
||||
use std::mem::size_of;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::eyre::Error;
|
||||
use color_eyre::Result;
|
||||
use std::path::Path;
|
||||
use windows::core::Result as WindowsCrateResult;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::core::PWSTR;
|
||||
@@ -47,6 +47,8 @@ use windows::Win32::Graphics::Gdi::HMONITOR;
|
||||
use windows::Win32::Graphics::Gdi::MONITORENUMPROC;
|
||||
use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
|
||||
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
|
||||
use windows::Win32::System::Com::CoCreateInstance;
|
||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::System::Power::RegisterPowerSettingNotification;
|
||||
use windows::Win32::System::Power::HPOWERNOTIFY;
|
||||
@@ -72,6 +74,9 @@ use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTUP;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEINPUT;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::VK_LBUTTON;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::VK_MENU;
|
||||
use windows::Win32::UI::Shell::DesktopWallpaper;
|
||||
use windows::Win32::UI::Shell::IDesktopWallpaper;
|
||||
use windows::Win32::UI::Shell::DWPOS_FILL;
|
||||
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::BringWindowToTop;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
|
||||
@@ -101,6 +106,7 @@ use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowPos;
|
||||
use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::ShowWindowAsync;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CW_USEDEFAULT;
|
||||
@@ -120,6 +126,7 @@ use windows::Win32::UI::WindowsAndMessaging::SPI_GETACTIVEWINDOWTRACKING;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_GETFOREGROUNDLOCKTIMEOUT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETFOREGROUNDLOCKTIMEOUT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_ASYNCWINDOWPOS;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_NOMOVE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_NOSIZE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
|
||||
@@ -141,6 +148,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
|
||||
use windows_core::BOOL;
|
||||
use windows_core::HSTRING;
|
||||
|
||||
use crate::core::Rect;
|
||||
|
||||
@@ -151,9 +159,12 @@ use crate::ring::Ring;
|
||||
use crate::set_window_position::SetWindowPosition;
|
||||
use crate::windows_callbacks;
|
||||
use crate::Window;
|
||||
use crate::WindowHandlingBehaviour;
|
||||
use crate::WindowManager;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::WINDOW_HANDLING_BEHAVIOUR;
|
||||
|
||||
macro_rules! as_ptr {
|
||||
($value:expr) => {
|
||||
@@ -258,7 +269,30 @@ impl WindowsApi {
|
||||
let monitors = &mut wm.monitors;
|
||||
let monitor_usr_idx_map = &mut wm.monitor_usr_idx_map;
|
||||
|
||||
'read: for display in win32_display_data::connected_displays_all().flatten() {
|
||||
let all_displays = win32_display_data::connected_displays_all()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut serial_id_map = HashMap::new();
|
||||
|
||||
for d in &all_displays {
|
||||
if let Some(id) = &d.serial_number_id {
|
||||
*serial_id_map.entry(id.clone()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for d in &all_displays {
|
||||
if let Some(id) = &d.serial_number_id {
|
||||
if serial_id_map.get(id).copied().unwrap_or_default() > 1 {
|
||||
let mut dupes = DUPLICATE_MONITOR_SERIAL_IDS.write();
|
||||
if !dupes.contains(id) {
|
||||
(*dupes).push(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
'read: for mut display in all_displays {
|
||||
let path = display.device_path.clone();
|
||||
|
||||
let (device, device_id) = if path.is_empty() {
|
||||
@@ -281,6 +315,13 @@ impl WindowsApi {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = &display.serial_number_id {
|
||||
let dupes = DUPLICATE_MONITOR_SERIAL_IDS.read();
|
||||
if dupes.contains(id) {
|
||||
display.serial_number_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
let m = monitor::new(
|
||||
display.hmonitor,
|
||||
display.size.into(),
|
||||
@@ -299,7 +340,7 @@ impl WindowsApi {
|
||||
}
|
||||
}
|
||||
|
||||
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
|
||||
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.read();
|
||||
for (index, id) in &*display_index_preferences {
|
||||
if m.serial_number_id().as_ref().is_some_and(|sn| sn == id) || id.eq(m.device_id())
|
||||
{
|
||||
@@ -334,7 +375,7 @@ impl WindowsApi {
|
||||
// Rebuild monitor index map
|
||||
*monitor_usr_idx_map = HashMap::new();
|
||||
let mut added_monitor_idxs = Vec::new();
|
||||
for (index, id) in &*DISPLAY_INDEX_PREFERENCES.lock() {
|
||||
for (index, id) in &*DISPLAY_INDEX_PREFERENCES.read() {
|
||||
if let Some(m_idx) = monitors.elements().iter().position(|m| {
|
||||
m.serial_number_id().as_ref().is_some_and(|sn| sn == id) || m.device_id() == id
|
||||
}) {
|
||||
@@ -436,7 +477,12 @@ impl WindowsApi {
|
||||
/// position window resizes the target window to the given layout, adjusting
|
||||
/// the layout to account for any window shadow borders (the window painted
|
||||
/// region will match layout on completion).
|
||||
pub fn position_window(hwnd: isize, layout: &Rect, top: bool) -> Result<()> {
|
||||
pub fn position_window(
|
||||
hwnd: isize,
|
||||
layout: &Rect,
|
||||
top: bool,
|
||||
with_async_window_pos: bool,
|
||||
) -> Result<()> {
|
||||
let hwnd = HWND(as_ptr!(hwnd));
|
||||
|
||||
let mut flags = SetWindowPosition::NO_ACTIVATE
|
||||
@@ -447,6 +493,19 @@ impl WindowsApi {
|
||||
// If the request is to place the window on top, then HWND_TOP will take
|
||||
// effect, otherwise pass NO_Z_ORDER that will cause set_window_pos to
|
||||
// ignore the z-order paramter.
|
||||
|
||||
// By default SetWindowPos waits for target window's WindowProc thread
|
||||
// to process the message, so we have to use ASYNC_WINDOW_POS to avoid
|
||||
// blocking our thread in case the target window is not responding.
|
||||
if with_async_window_pos
|
||||
&& matches!(
|
||||
WINDOW_HANDLING_BEHAVIOUR.load(),
|
||||
WindowHandlingBehaviour::Async
|
||||
)
|
||||
{
|
||||
flags |= SetWindowPosition::ASYNC_WINDOW_POS;
|
||||
}
|
||||
|
||||
if !top {
|
||||
flags |= SetWindowPosition::NO_Z_ORDER;
|
||||
}
|
||||
@@ -481,11 +540,18 @@ impl WindowsApi {
|
||||
/// Raise the window to the top of the Z order, but do not activate or focus
|
||||
/// it. Use raise_and_focus_window to activate and focus a window.
|
||||
pub fn raise_window(hwnd: isize) -> Result<()> {
|
||||
let flags = SetWindowPosition::NO_MOVE
|
||||
let mut flags = SetWindowPosition::NO_MOVE
|
||||
| SetWindowPosition::NO_SIZE
|
||||
| SetWindowPosition::NO_ACTIVATE
|
||||
| SetWindowPosition::SHOW_WINDOW;
|
||||
|
||||
if matches!(
|
||||
WINDOW_HANDLING_BEHAVIOUR.load(),
|
||||
WindowHandlingBehaviour::Async
|
||||
) {
|
||||
flags |= SetWindowPosition::ASYNC_WINDOW_POS;
|
||||
}
|
||||
|
||||
let position = HWND_TOP;
|
||||
Self::set_window_pos(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
@@ -498,11 +564,18 @@ impl WindowsApi {
|
||||
/// Lower the window to the bottom of the Z order, but do not activate or focus
|
||||
/// it.
|
||||
pub fn lower_window(hwnd: isize) -> Result<()> {
|
||||
let flags = SetWindowPosition::NO_MOVE
|
||||
let mut flags = SetWindowPosition::NO_MOVE
|
||||
| SetWindowPosition::NO_SIZE
|
||||
| SetWindowPosition::NO_ACTIVATE
|
||||
| SetWindowPosition::SHOW_WINDOW;
|
||||
|
||||
if matches!(
|
||||
WINDOW_HANDLING_BEHAVIOUR.load(),
|
||||
WindowHandlingBehaviour::Async
|
||||
) {
|
||||
flags |= SetWindowPosition::ASYNC_WINDOW_POS;
|
||||
}
|
||||
|
||||
let position = HWND_BOTTOM;
|
||||
Self::set_window_pos(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
@@ -513,12 +586,17 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn set_border_pos(hwnd: isize, layout: &Rect, position: isize) -> Result<()> {
|
||||
let flags = {
|
||||
SetWindowPosition::NO_SEND_CHANGING
|
||||
| SetWindowPosition::NO_ACTIVATE
|
||||
| SetWindowPosition::NO_REDRAW
|
||||
| SetWindowPosition::SHOW_WINDOW
|
||||
};
|
||||
let mut flags = SetWindowPosition::NO_SEND_CHANGING
|
||||
| SetWindowPosition::NO_ACTIVATE
|
||||
| SetWindowPosition::NO_REDRAW
|
||||
| SetWindowPosition::SHOW_WINDOW;
|
||||
|
||||
if matches!(
|
||||
WINDOW_HANDLING_BEHAVIOUR.load(),
|
||||
WindowHandlingBehaviour::Async
|
||||
) {
|
||||
flags |= SetWindowPosition::ASYNC_WINDOW_POS;
|
||||
}
|
||||
|
||||
Self::set_window_pos(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
@@ -544,6 +622,7 @@ impl WindowsApi {
|
||||
.process()
|
||||
}
|
||||
|
||||
/// move_windows calls MoveWindow, but cannot be called with async window pos, so it might hang
|
||||
pub fn move_window(hwnd: isize, layout: &Rect, repaint: bool) -> Result<()> {
|
||||
let hwnd = HWND(as_ptr!(hwnd));
|
||||
|
||||
@@ -561,9 +640,18 @@ impl WindowsApi {
|
||||
// BOOL is returned but does not signify whether or not the operation was succesful
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
|
||||
// TODO: error handling
|
||||
unsafe {
|
||||
let _ = ShowWindow(HWND(as_ptr!(hwnd)), command);
|
||||
};
|
||||
if matches!(
|
||||
WINDOW_HANDLING_BEHAVIOUR.load(),
|
||||
WindowHandlingBehaviour::Async
|
||||
) {
|
||||
unsafe {
|
||||
let _ = ShowWindowAsync(HWND(as_ptr!(hwnd)), command);
|
||||
};
|
||||
} else {
|
||||
unsafe {
|
||||
let _ = ShowWindow(HWND(as_ptr!(hwnd)), command);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn minimize_window(hwnd: isize) {
|
||||
@@ -619,7 +707,7 @@ impl WindowsApi {
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_ASYNCWINDOWPOS,
|
||||
)
|
||||
.process();
|
||||
SetForegroundWindow(HWND(as_ptr!(hwnd)))
|
||||
@@ -902,7 +990,7 @@ impl WindowsApi {
|
||||
pub fn exe(handle: HANDLE) -> Result<String> {
|
||||
Ok(Self::exe_path(handle)?
|
||||
.split('\\')
|
||||
.last()
|
||||
.next_back()
|
||||
.ok_or_else(|| anyhow!("there is no last element"))?
|
||||
.to_string())
|
||||
}
|
||||
@@ -971,8 +1059,18 @@ impl WindowsApi {
|
||||
Ok(ex_info)
|
||||
}
|
||||
|
||||
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
|
||||
pub fn monitor_device_path(hmonitor: isize) -> Option<String> {
|
||||
for display in win32_display_data::connected_displays_all().flatten() {
|
||||
if display.hmonitor == hmonitor {
|
||||
return Some(display.device_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
|
||||
for mut display in win32_display_data::connected_displays_all().flatten() {
|
||||
if display.hmonitor == hmonitor {
|
||||
let path = display.device_path;
|
||||
|
||||
@@ -990,6 +1088,13 @@ impl WindowsApi {
|
||||
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
|
||||
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
|
||||
|
||||
if let Some(id) = &display.serial_number_id {
|
||||
let dupes = DUPLICATE_MONITOR_SERIAL_IDS.read();
|
||||
if dupes.contains(id) {
|
||||
display.serial_number_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
let monitor = monitor::new(
|
||||
hmonitor,
|
||||
display.size.into(),
|
||||
@@ -1307,4 +1412,47 @@ impl WindowsApi {
|
||||
pub fn wts_register_session_notification(hwnd: isize) -> Result<()> {
|
||||
unsafe { WTSRegisterSessionNotification(HWND(as_ptr!(hwnd)), 1) }.process()
|
||||
}
|
||||
|
||||
pub fn set_wallpaper(path: &Path, hmonitor: isize) -> Result<()> {
|
||||
let path = path.canonicalize()?;
|
||||
|
||||
let wallpaper: IDesktopWallpaper =
|
||||
unsafe { CoCreateInstance(&DesktopWallpaper, None, CLSCTX_ALL)? };
|
||||
|
||||
let wallpaper_path = HSTRING::from(path.to_str().unwrap_or_default());
|
||||
unsafe {
|
||||
wallpaper.SetPosition(DWPOS_FILL)?;
|
||||
}
|
||||
|
||||
let monitor_id = if let Some(path) = Self::monitor_device_path(hmonitor) {
|
||||
PCWSTR::from_raw(HSTRING::from(path).as_ptr())
|
||||
} else {
|
||||
PCWSTR::null()
|
||||
};
|
||||
|
||||
// Set the wallpaper
|
||||
unsafe {
|
||||
wallpaper.SetWallpaper(monitor_id, PCWSTR::from_raw(wallpaper_path.as_ptr()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_wallpaper(hmonitor: isize) -> Result<String> {
|
||||
let wallpaper: IDesktopWallpaper =
|
||||
unsafe { CoCreateInstance(&DesktopWallpaper, None, CLSCTX_ALL)? };
|
||||
|
||||
let monitor_id = if let Some(path) = Self::monitor_device_path(hmonitor) {
|
||||
PCWSTR::from_raw(HSTRING::from(path).as_ptr())
|
||||
} else {
|
||||
PCWSTR::null()
|
||||
};
|
||||
|
||||
// Set the wallpaper
|
||||
unsafe {
|
||||
wallpaper
|
||||
.GetWallpaper(monitor_id)
|
||||
.and_then(|pwstr| pwstr.to_string().map_err(|e| e.into()))
|
||||
}
|
||||
.process()
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user