mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-28 14:38:14 +01:00
Compare commits
1 Commits
feature/sc
...
feature/sp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fed31bc54 |
2
.github/workflows/feature-check.yaml
vendored
2
.github/workflows/feature-check.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check and close feature issues
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
17
.github/workflows/windows.yaml
vendored
17
.github/workflows/windows.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
@@ -43,11 +43,10 @@ jobs:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: rustup toolchain install stable --profile minimal
|
||||
- run: rustup component add --toolchain stable-x86_64-pc-windows-msvc clippy
|
||||
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -82,12 +81,12 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v4
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
@@ -129,14 +128,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v4
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
@@ -171,14 +170,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v4
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
|
||||
3473
Cargo.lock
generated
3473
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -8,8 +8,7 @@ members = [
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes",
|
||||
"komorebi-shortcuts",
|
||||
"komorebi-themes"
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -19,8 +18,8 @@ chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.32"
|
||||
egui_extras = "0.32"
|
||||
eframe = "0.31"
|
||||
egui_extras = "0.31"
|
||||
dirs = "6"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
@@ -33,20 +32,19 @@ 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.37"
|
||||
sysinfo = "0.33"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
|
||||
windows-numerics = { version = "0.3" }
|
||||
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.62" }
|
||||
windows-core = { version = "0.61" }
|
||||
shadow-rs = "1"
|
||||
which = "8"
|
||||
which = "7"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.62"
|
||||
version = "0.61"
|
||||
features = [
|
||||
"Foundation_Numerics",
|
||||
"Win32_Devices",
|
||||
@@ -73,12 +71,5 @@ features = [
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control",
|
||||
]
|
||||
|
||||
[profile.release-opt]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
"Media_Control"
|
||||
]
|
||||
@@ -394,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.38"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.35"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
|
||||
40
deny.toml
40
deny.toml
@@ -13,9 +13,7 @@ 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-0320", reason = "not using any yaml features from this library" },
|
||||
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" }
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" }
|
||||
]
|
||||
|
||||
[licenses]
|
||||
@@ -35,58 +33,43 @@ allow = [
|
||||
"Ubuntu-font-1.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"LicenseRef-Komorebi-2.0"
|
||||
"LicenseRef-Komorebi-1.0"
|
||||
]
|
||||
confidence-threshold = 0.8
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-client"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebic"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebic-no-console"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-themes"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-gui"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-bar"
|
||||
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"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
@@ -107,10 +90,7 @@ unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = [
|
||||
"https://github.com/LGUG2Z/base16-egui-themes",
|
||||
"https://github.com/LGUG2Z/catppuccin-egui",
|
||||
"https://github.com/LGUG2Z/windows-icons",
|
||||
"https://github.com/LGUG2Z/win32-display-data",
|
||||
"https://github.com/LGUG2Z/flavours",
|
||||
"https://github.com/LGUG2Z/base16_color_scheme",
|
||||
"https://github.com/LGUG2Z/whkd",
|
||||
# "https://github.com/LGUG2Z/catppuccin-egui",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,9 @@ 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
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
|
||||
|
||||
Arguments:
|
||||
<DEFAULT_LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# clear-session-float-rules
|
||||
|
||||
```
|
||||
Clear all session float rules
|
||||
|
||||
Usage: komorebic.exe clear-session-float-rules
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,12 +0,0 @@
|
||||
# data-directory
|
||||
|
||||
```
|
||||
Show the path to komorebi's data directory in %LOCALAPPDATA%
|
||||
|
||||
Usage: komorebic.exe data-directory
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -10,7 +10,7 @@ Arguments:
|
||||
Target workspace name
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --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, focused-workspace-layout, focused-container-kind, version]
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# scrolling-layout-columns
|
||||
|
||||
```
|
||||
Set the number of visible columns for the Scrolling layout on the focused workspace
|
||||
|
||||
Usage: komorebic.exe scrolling-layout-columns <COUNT>
|
||||
|
||||
Arguments:
|
||||
<COUNT>
|
||||
Desired number of visible columns
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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
|
||||
|
||||
```
|
||||
@@ -1,12 +0,0 @@
|
||||
# session-float-rules
|
||||
|
||||
```
|
||||
Show all session float rules
|
||||
|
||||
Usage: komorebic.exe session-float-rules
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# toggle-pause
|
||||
|
||||
```
|
||||
Toggle the paused state for all window tiling
|
||||
Toggle window tiling on the focused workspace
|
||||
|
||||
Usage: komorebic.exe toggle-pause
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# toggle-shortcuts
|
||||
|
||||
```
|
||||
Toggle the komorebi-shortcuts helper
|
||||
|
||||
Usage: komorebic.exe toggle-shortcuts
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,8 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Usage: komorebic.exe window-hiding-behaviour <HIDING_BEHAVIOUR>
|
||||
Arguments:
|
||||
<HIDING_BEHAVIOUR>
|
||||
Possible values:
|
||||
- hide: END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# workspace-work-area-offset
|
||||
|
||||
```
|
||||
Set offsets for a workspace to exclude parts of the work area from tiling
|
||||
|
||||
Usage: komorebic.exe workspace-work-area-offset <MONITOR> <WORKSPACE> <LEFT> <TOP> <RIGHT> <BOTTOM>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index (zero-indexed)
|
||||
|
||||
<LEFT>
|
||||
Size of the left work area offset (set right to left * 2 to maintain right padding)
|
||||
|
||||
<TOP>
|
||||
Size of the top work area offset (set bottom to the same value to maintain bottom padding)
|
||||
|
||||
<RIGHT>
|
||||
Size of the right work area offset
|
||||
|
||||
<BOTTOM>
|
||||
Size of the bottom work area offset
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -6,10 +6,7 @@ defined in the `komorebi.json` configuration file.
|
||||
```json
|
||||
{
|
||||
"animation": {
|
||||
"enabled": true,
|
||||
"duration": 250,
|
||||
"fps": 60,
|
||||
"style": "EaseOutSine"
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -301,7 +301,7 @@ how to map the indices and would use default behaviour which would result in a m
|
||||
}
|
||||
```
|
||||
|
||||
# Multiple monitors on different machines
|
||||
# Multiple Monitors on different machines
|
||||
|
||||
You can use the same `komorebi.json` to configure two different setups and then synchronize your config across machines.
|
||||
However, if you do this it is important to be aware of a few things.
|
||||
@@ -393,13 +393,6 @@ This is because komorebi will apply the appropriate config to the loaded monitor
|
||||
index (the index defined in the user config) to the actual monitor index, and the bar will use that map to know if it
|
||||
should be enabled, and where it should be drawn.
|
||||
|
||||
# Windows Display Settings
|
||||
|
||||
In `Settings > System > Display > Multiple Displays`:
|
||||
|
||||
- Disable "Remember windows locations on monitor connection"
|
||||
- Enable "Minimize windows when a monitor is disconnected"
|
||||
|
||||
### Things to keep in mind
|
||||
|
||||
* If you are using a laptop connected to one monitor at work and a different one at home, the work monitor and the home
|
||||
|
||||
@@ -8,8 +8,12 @@ configuration file.
|
||||
```json
|
||||
{
|
||||
"default_workspace_padding": 0,
|
||||
"default_container_padding": -1,
|
||||
"default_container_padding": 0,
|
||||
"border_width": 0,
|
||||
"border_offset": -1
|
||||
}
|
||||
```
|
||||
|
||||
A restart of `komorebi` is required after changing these settings.
|
||||
|
||||
[](https://www.youtube.com/watch?v=6QYLao953XE)
|
||||
|
||||
17
docs/common-workflows/set-display-index.md
Normal file
17
docs/common-workflows/set-display-index.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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,9 +186,6 @@ limitations on hotkey bindings that include the `win` key. However, you will sti
|
||||
to [modify the registry](https://superuser.com/questions/1059511/how-to-disable-winl-in-windows-10) to prevent
|
||||
`win + l` from locking the operating system.
|
||||
|
||||
You can toggle an overlay of the current `whkdrc` shortcuts related to `komorebi` at any time when using the example
|
||||
configuration with `alt + i`.
|
||||
|
||||
```
|
||||
{% include "./whkdrc.sample" %}
|
||||
```
|
||||
|
||||
@@ -120,7 +120,6 @@ 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.39/schema.bar.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.35/schema.bar.json",
|
||||
"monitor": 0,
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.39/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.35/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
@@ -14,6 +14,13 @@
|
||||
"unfocused_border": "Base03",
|
||||
"bar_accent": "Base0D"
|
||||
},
|
||||
"stackbar": {
|
||||
"height": 40,
|
||||
"mode": "OnStack",
|
||||
"tabs": {
|
||||
"width": 300
|
||||
}
|
||||
},
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
|
||||
@@ -132,20 +132,3 @@ 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.
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
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 komorebi-shortcuts
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
install-with-jsonschema:
|
||||
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
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 komorebi-shortcuts
|
||||
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
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 komorebi-shortcuts
|
||||
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
run target:
|
||||
cargo +stable run --bin {{ target }} --locked --no-default-features
|
||||
@@ -88,6 +88,3 @@ 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,13 +1,13 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.39"
|
||||
edition = "2024"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
komorebi-themes = { path = "../komorebi-themes", default-features = false }
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
|
||||
chrono-tz = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
@@ -17,16 +17,15 @@ crossbeam-channel = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui-phosphor = "0.10"
|
||||
egui-phosphor = "0.9"
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
lazy_static = { workspace = true }
|
||||
netdev = "0.36"
|
||||
netdev = "0.33"
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
parking_lot = { workspace = true }
|
||||
random_word = { version = "0.5", features = ["en"] }
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
schemars = { workspace = true, optional = true }
|
||||
@@ -36,7 +35,6 @@ starship-battery = "0.10"
|
||||
sysinfo = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
which = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" }
|
||||
@@ -44,4 +42,4 @@ windows-icons-fallback = { package = "windows-icons", git = "https://github.com/
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["dep:schemars", "komorebi-client/default", "komorebi-themes/default"]
|
||||
schemars = ["dep:schemars"]
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||
use crate::BAR_HEIGHT;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use crate::KomorebiEvent;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_LEFT;
|
||||
use crate::MONITOR_RIGHT;
|
||||
use crate::MONITOR_TOP;
|
||||
use crate::config::get_individual_spacing;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use crate::config::get_individual_spacing;
|
||||
use crate::process_hwnd;
|
||||
use crate::render::Color32Ext;
|
||||
use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::render::RenderExt;
|
||||
use crate::widgets::komorebi::Komorebi;
|
||||
use crate::widgets::komorebi::MonitorInfo;
|
||||
use crate::widgets::komorebi::KomorebiNotificationState;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::widgets::widget::WidgetConfig;
|
||||
use color_eyre::eyre;
|
||||
use crate::KomorebiEvent;
|
||||
use crate::BAR_HEIGHT;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_LEFT;
|
||||
use crate::MONITOR_RIGHT;
|
||||
use crate::MONITOR_TOP;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use eframe::egui::Align;
|
||||
@@ -39,7 +36,6 @@ 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;
|
||||
@@ -47,99 +43,20 @@ 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::Base16Wrapper;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use komorebi_themes::Base16Value;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::CatppuccinValue;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Error;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Write;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ChildStdin;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
lazy_static! {
|
||||
static ref SESSION_STDIN: Mutex<Option<ChildStdin>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
fn start_powershell() -> eyre::Result<()> {
|
||||
// found running session, do nothing
|
||||
if SESSION_STDIN.lock().as_mut().is_some() {
|
||||
tracing::debug!("PowerShell session already started");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::debug!("Starting PowerShell session");
|
||||
|
||||
let mut child = Command::new("powershell.exe")
|
||||
.args(["-NoLogo", "-NoProfile", "-Command", "-"])
|
||||
.stdin(Stdio::piped())
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().expect("stdin piped");
|
||||
|
||||
// Store stdin for later commands
|
||||
let mut session_stdin = SESSION_STDIN.lock();
|
||||
*session_stdin = Option::from(stdin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_powershell() -> eyre::Result<()> {
|
||||
tracing::debug!("Stopping PowerShell session");
|
||||
|
||||
if let Some(mut session_stdin) = SESSION_STDIN.lock().take() {
|
||||
if let Err(e) = session_stdin.write_all(b"exit\n") {
|
||||
tracing::error!(error = %e, "failed to write exit command to PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
if let Err(e) = session_stdin.flush() {
|
||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
tracing::debug!("PowerShell session stopped");
|
||||
} else {
|
||||
tracing::debug!("PowerShell session already stopped");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_powershell(cmd: &str) -> eyre::Result<()> {
|
||||
if let Some(session_stdin) = SESSION_STDIN.lock().as_mut() {
|
||||
if let Err(e) = writeln!(session_stdin, "{cmd}") {
|
||||
tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
if let Err(e) = session_stdin.flush() {
|
||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(Error::new(ErrorKind::NotFound, "PowerShell session not started").into())
|
||||
}
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Komobar {
|
||||
pub hwnd: Option<isize>,
|
||||
@@ -147,7 +64,7 @@ pub struct Komobar {
|
||||
pub disabled: bool,
|
||||
pub config: KomobarConfig,
|
||||
pub render_config: Rc<RefCell<RenderConfig>>,
|
||||
pub monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
||||
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub center_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
||||
@@ -159,18 +76,6 @@ 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(
|
||||
@@ -182,86 +87,75 @@ pub fn apply_theme(
|
||||
grouping: Option<Grouping>,
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
let (auto_select_fill, auto_select_text) = match theme {
|
||||
match theme {
|
||||
KomobarTheme::Catppuccin {
|
||||
name: catppuccin,
|
||||
accent: catppuccin_value,
|
||||
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());
|
||||
} => 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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());
|
||||
|
||||
(
|
||||
catppuccin_auto_select_fill.map(|c| c.color32(catppuccin.as_theme())),
|
||||
catppuccin_auto_select_text.map(|c| c.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);
|
||||
}
|
||||
},
|
||||
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(Base16Wrapper::Base16(base16));
|
||||
let accent = base16_value.color32(base16);
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
@@ -270,46 +164,8 @@ 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();
|
||||
@@ -319,15 +175,16 @@ pub fn apply_theme(
|
||||
// apply rounding to the widgets
|
||||
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) =
|
||||
&grouping
|
||||
&& let Some(rounding) = config.rounding
|
||||
{
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
if let Some(rounding) = config.rounding {
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update RenderConfig's background_color so that widgets will have the new color
|
||||
@@ -338,7 +195,7 @@ impl Komobar {
|
||||
pub fn apply_config(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
previous_monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
||||
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
) {
|
||||
MAX_LABEL_WIDTH.store(
|
||||
self.config.max_label_width.unwrap_or(400.0) as i32,
|
||||
@@ -367,7 +224,7 @@ impl Komobar {
|
||||
self.config.icon_scale,
|
||||
));
|
||||
|
||||
let mut monitor_info = previous_monitor_info;
|
||||
let mut komorebi_notification_state = previous_notification_state;
|
||||
let mut komorebi_widgets = Vec::new();
|
||||
|
||||
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
|
||||
@@ -419,18 +276,19 @@ impl Komobar {
|
||||
komorebi_widgets
|
||||
.into_iter()
|
||||
.for_each(|(mut widget, idx, side)| {
|
||||
match monitor_info {
|
||||
match komorebi_notification_state {
|
||||
None => {
|
||||
monitor_info = Some(widget.monitor_info.clone());
|
||||
komorebi_notification_state =
|
||||
Some(widget.komorebi_notification_state.clone());
|
||||
}
|
||||
Some(ref previous) => {
|
||||
if widget.workspaces.is_some() {
|
||||
previous
|
||||
.borrow_mut()
|
||||
.update_from_self(&widget.monitor_info.borrow());
|
||||
if widget.workspaces.is_some_and(|w| w.enable) {
|
||||
previous.borrow_mut().update_from_config(
|
||||
&widget.komorebi_notification_state.borrow(),
|
||||
);
|
||||
}
|
||||
|
||||
widget.monitor_info = previous.clone();
|
||||
widget.komorebi_notification_state = previous.clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,19 +313,15 @@ impl Komobar {
|
||||
}
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
};
|
||||
|
||||
let mapped_info = self.monitor_info.as_ref().map(|info| {
|
||||
let monitor = info.borrow();
|
||||
(
|
||||
monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
|
||||
monitor.mouse_follows_focus,
|
||||
)
|
||||
let monitor_index = self.komorebi_notification_state.as_ref().and_then(|state| {
|
||||
state
|
||||
.borrow()
|
||||
.monitor_usr_idx_map
|
||||
.get(&usr_monitor_index)
|
||||
.copied()
|
||||
});
|
||||
|
||||
if let Some(info) = mapped_info {
|
||||
self.monitor_index = info.0;
|
||||
self.mouse_follows_focus = info.1;
|
||||
}
|
||||
self.monitor_index = monitor_index;
|
||||
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
|
||||
@@ -518,51 +372,17 @@ impl Komobar {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.monitor_info.is_some() && !self.disabled {
|
||||
tracing::warn!(
|
||||
"couldn't find the monitor index of this bar! Disabling the bar until the monitor connects..."
|
||||
);
|
||||
} else if self.komorebi_notification_state.is_some() && !self.disabled {
|
||||
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
|
||||
self.disabled = true;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi."
|
||||
);
|
||||
tracing::warn!("couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi.");
|
||||
self.disabled = true;
|
||||
}
|
||||
|
||||
if let Some(mouse) = &self.config.mouse {
|
||||
self.input_config.act_on_vertical_scroll =
|
||||
mouse.on_scroll_up.is_some() || mouse.on_scroll_down.is_some();
|
||||
self.input_config.act_on_horizontal_scroll =
|
||||
mouse.on_scroll_left.is_some() || mouse.on_scroll_right.is_some();
|
||||
self.input_config.vertical_scroll_threshold = mouse
|
||||
.vertical_scroll_threshold
|
||||
.unwrap_or(30.0)
|
||||
.clamp(10.0, 300.0);
|
||||
self.input_config.horizontal_scroll_threshold = mouse
|
||||
.horizontal_scroll_threshold
|
||||
.unwrap_or(30.0)
|
||||
.clamp(10.0, 300.0);
|
||||
// limit how many "ticks" can be accumulated
|
||||
self.input_config.vertical_scroll_max_threshold =
|
||||
self.input_config.vertical_scroll_threshold * 3.0;
|
||||
self.input_config.horizontal_scroll_max_threshold =
|
||||
self.input_config.horizontal_scroll_threshold * 3.0;
|
||||
|
||||
if mouse.has_command() {
|
||||
start_powershell().unwrap_or_else(|_| {
|
||||
tracing::error!("failed to start powershell session");
|
||||
});
|
||||
} else {
|
||||
stop_powershell().unwrap_or_else(|_| {
|
||||
tracing::error!("failed to stop powershell session");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("widget configuration options applied");
|
||||
|
||||
self.monitor_info = monitor_info;
|
||||
self.komorebi_notification_state = komorebi_notification_state;
|
||||
}
|
||||
|
||||
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
|
||||
@@ -599,9 +419,7 @@ impl Komobar {
|
||||
end.x -= margin.left + margin.right;
|
||||
|
||||
if end.y == 0.0 {
|
||||
tracing::warn!(
|
||||
"position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default"
|
||||
)
|
||||
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
|
||||
}
|
||||
|
||||
self.size_rect = komorebi_client::Rect {
|
||||
@@ -613,11 +431,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.clone(),
|
||||
theme,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
@@ -629,15 +447,13 @@ 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 = home_path.replace_env();
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
home
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -656,6 +472,21 @@ impl Komobar {
|
||||
bar_grouping,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -668,16 +499,17 @@ impl Komobar {
|
||||
| Grouping::Alignment(config)
|
||||
| Grouping::Widget(config),
|
||||
) = &bar_grouping
|
||||
&& let Some(rounding) = config.rounding
|
||||
{
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius =
|
||||
rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
if let Some(rounding) = config.rounding {
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius =
|
||||
rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -697,7 +529,7 @@ impl Komobar {
|
||||
disabled: false,
|
||||
config,
|
||||
render_config: Rc::new(RefCell::new(RenderConfig::new())),
|
||||
monitor_info: None,
|
||||
komorebi_notification_state: None,
|
||||
left_widgets: vec![],
|
||||
center_widgets: vec![],
|
||||
right_widgets: vec![],
|
||||
@@ -709,16 +541,6 @@ 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);
|
||||
@@ -843,12 +665,12 @@ impl eframe::App for Komobar {
|
||||
|
||||
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
|
||||
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
}
|
||||
|
||||
if let Ok(updated_config) = self.rx_config.try_recv() {
|
||||
self.config = updated_config;
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
}
|
||||
|
||||
match self.rx_gui.try_recv() {
|
||||
@@ -870,30 +692,6 @@ 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
|
||||
@@ -938,9 +736,9 @@ impl eframe::App for Komobar {
|
||||
) {
|
||||
let monitor_index = self.monitor_index.expect("should have a monitor index");
|
||||
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size;
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size();
|
||||
|
||||
self.update_monitor_coordinates(&monitor_size);
|
||||
self.update_monitor_coordinates(monitor_size);
|
||||
|
||||
should_apply_config = true;
|
||||
}
|
||||
@@ -951,7 +749,7 @@ impl eframe::App for Komobar {
|
||||
|
||||
// Check if monitor coordinates/size has changed
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size;
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size();
|
||||
let top = MONITOR_TOP.load(Ordering::SeqCst);
|
||||
let left = MONITOR_LEFT.load(Ordering::SeqCst);
|
||||
let right = MONITOR_RIGHT.load(Ordering::SeqCst);
|
||||
@@ -961,38 +759,36 @@ impl eframe::App for Komobar {
|
||||
bottom: monitor_size.bottom,
|
||||
right,
|
||||
};
|
||||
if monitor_size != rect {
|
||||
if *monitor_size != rect {
|
||||
tracing::info!(
|
||||
"Monitor coordinates/size has changed, storing new coordinates: {:#?}",
|
||||
monitor_size
|
||||
);
|
||||
|
||||
self.update_monitor_coordinates(&monitor_size);
|
||||
self.update_monitor_coordinates(monitor_size);
|
||||
|
||||
should_apply_config = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(monitor_info) = &self.monitor_info {
|
||||
monitor_info.borrow_mut().update(
|
||||
self.monitor_index,
|
||||
notification.state,
|
||||
self.render_config.borrow().show_all_icons,
|
||||
);
|
||||
handle_notification(
|
||||
ctx,
|
||||
notification.event,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme.clone(),
|
||||
self.render_config.clone(),
|
||||
);
|
||||
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
|
||||
komorebi_notification_state
|
||||
.borrow_mut()
|
||||
.handle_notification(
|
||||
ctx,
|
||||
self.monitor_index,
|
||||
notification,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if should_apply_config {
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
|
||||
// Reposition the Bar
|
||||
self.position_bar();
|
||||
@@ -1074,111 +870,6 @@ 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
|
||||
@@ -1318,66 +1009,3 @@ pub enum Alignment {
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn handle_notification(
|
||||
ctx: &Context,
|
||||
event: komorebi_client::NotificationEvent,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
bg_color_with_alpha: Rc<RefCell<Color32>>,
|
||||
transparency_alpha: Option<u8>,
|
||||
grouping: Option<Grouping>,
|
||||
default_theme: Option<KomobarTheme>,
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
if let NotificationEvent::Socket(message) = event {
|
||||
match message {
|
||||
SocketMessage::ReloadStaticConfiguration(path) => {
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
} else if let Some(default_theme) = default_theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
default_theme,
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!(
|
||||
"removed theme from updated komorebi.json and applied default theme"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"theme was removed from updated komorebi.json but there was no default theme to apply"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(*theme),
|
||||
bg_color,
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use crate::DEFAULT_PADDING;
|
||||
use crate::bar::exec_powershell;
|
||||
use crate::render::Grouping;
|
||||
use crate::widgets::widget::WidgetConfig;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -16,7 +13,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.39`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.36`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar height (default: 50)
|
||||
pub height: Option<f32>,
|
||||
@@ -93,8 +90,6 @@ 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)
|
||||
@@ -120,9 +115,7 @@ impl KomobarConfig {
|
||||
}
|
||||
|
||||
if display {
|
||||
println!(
|
||||
"\nYour bar configuration file contains some options that have been renamed or deprecated:\n"
|
||||
);
|
||||
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
|
||||
for (canonical, aliases) in map {
|
||||
for alias in aliases {
|
||||
if raw.contains(alias) {
|
||||
@@ -332,146 +325,6 @@ pub fn get_individual_spacing(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum MouseMessage {
|
||||
/// Send a message to the komorebi client.
|
||||
/// By default, a batch of messages are sent in the following order:
|
||||
/// FocusMonitorAtCursor =>
|
||||
/// MouseFollowsFocus(false) =>
|
||||
/// {message} =>
|
||||
/// MouseFollowsFocus({original.value})
|
||||
///
|
||||
/// Example:
|
||||
/// ```json
|
||||
/// "on_extra2_click": {
|
||||
/// "message": {
|
||||
/// "type": "NewWorkspace"
|
||||
/// }
|
||||
/// },
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "on_middle_click": {
|
||||
/// "focus_monitor_at_cursor": false,
|
||||
/// "ignore_mouse_follows_focus": false,
|
||||
/// "message": {
|
||||
/// "type": "TogglePause"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "on_scroll_up": {
|
||||
/// "message": {
|
||||
/// "type": "CycleFocusWorkspace",
|
||||
/// "content": "Previous"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
Komorebi(KomorebiMouseMessage),
|
||||
/// Execute a custom command.
|
||||
/// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved.
|
||||
/// Example: `komorebic toggle-pause`
|
||||
Command(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiMouseMessage {
|
||||
/// Send the FocusMonitorAtCursor message (default:true)
|
||||
pub focus_monitor_at_cursor: Option<bool>,
|
||||
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message (default:true)
|
||||
pub ignore_mouse_follows_focus: Option<bool>,
|
||||
/// The message to send to the komorebi client
|
||||
pub message: komorebi_client::SocketMessage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct MouseConfig {
|
||||
/// Command to send on primary/left double button click
|
||||
pub on_primary_double_click: Option<MouseMessage>,
|
||||
/// Command to send on secondary/right button click
|
||||
pub on_secondary_click: Option<MouseMessage>,
|
||||
/// Command to send on middle button click
|
||||
pub on_middle_click: Option<MouseMessage>,
|
||||
/// Command to send on extra1/back button click
|
||||
pub on_extra1_click: Option<MouseMessage>,
|
||||
/// Command to send on extra2/forward button click
|
||||
pub on_extra2_click: Option<MouseMessage>,
|
||||
|
||||
/// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
||||
pub vertical_scroll_threshold: Option<f32>,
|
||||
/// Command to send on scrolling up (every tick)
|
||||
pub on_scroll_up: Option<MouseMessage>,
|
||||
/// Command to send on scrolling down (every tick)
|
||||
pub on_scroll_down: Option<MouseMessage>,
|
||||
|
||||
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
||||
pub horizontal_scroll_threshold: Option<f32>,
|
||||
/// Command to send on scrolling left (every tick)
|
||||
pub on_scroll_left: Option<MouseMessage>,
|
||||
/// Command to send on scrolling right (every tick)
|
||||
pub on_scroll_right: Option<MouseMessage>,
|
||||
}
|
||||
|
||||
impl MouseConfig {
|
||||
pub fn has_command(&self) -> bool {
|
||||
[
|
||||
&self.on_primary_double_click,
|
||||
&self.on_secondary_click,
|
||||
&self.on_middle_click,
|
||||
&self.on_extra1_click,
|
||||
&self.on_extra2_click,
|
||||
&self.on_scroll_up,
|
||||
&self.on_scroll_down,
|
||||
&self.on_scroll_left,
|
||||
&self.on_scroll_right,
|
||||
]
|
||||
.iter()
|
||||
.any(|opt| matches!(opt, Some(MouseMessage::Command(_))))
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseMessage {
|
||||
pub fn execute(&self, mouse_follows_focus: bool) {
|
||||
match self {
|
||||
MouseMessage::Komorebi(config) => {
|
||||
let mut messages = Vec::new();
|
||||
|
||||
if config.focus_monitor_at_cursor.unwrap_or(true) {
|
||||
messages.push(SocketMessage::FocusMonitorAtCursor);
|
||||
}
|
||||
|
||||
if config.ignore_mouse_follows_focus.unwrap_or(true) {
|
||||
messages.push(SocketMessage::MouseFollowsFocus(false));
|
||||
messages.push(config.message.clone());
|
||||
messages.push(SocketMessage::MouseFollowsFocus(mouse_follows_focus));
|
||||
} else {
|
||||
messages.push(config.message.clone());
|
||||
}
|
||||
|
||||
tracing::debug!("Sending messages: {messages:?}");
|
||||
|
||||
if komorebi_client::send_batch(messages).is_err() {
|
||||
tracing::error!("could not send commands");
|
||||
}
|
||||
}
|
||||
MouseMessage::Command(cmd) => {
|
||||
tracing::debug!("Executing command: {}", cmd);
|
||||
|
||||
let cmd_no_env = cmd.replace_env();
|
||||
|
||||
if exec_powershell(cmd_no_env.to_str().expect("Invalid command")).is_err() {
|
||||
tracing::error!("Failed to execute '{}'", cmd);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
@@ -520,7 +373,7 @@ impl From<Position> for Pos2 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
pub enum KomobarTheme {
|
||||
@@ -529,24 +382,12 @@ 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>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -558,26 +399,12 @@ 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,25 +15,26 @@ use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use komorebi_client::PathExt;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::SubscribeOptions;
|
||||
use komorebi_client::replace_env_in_path;
|
||||
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;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||
use windows::Win32::System::Threading::GetCurrentThreadId;
|
||||
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
|
||||
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
use windows_core::BOOL;
|
||||
@@ -46,8 +47,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 AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
@@ -60,7 +61,6 @@ 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)]
|
||||
@@ -114,14 +114,14 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
if opts.schema {
|
||||
let settings = schemars::r#gen::SchemaSettings::default().with(|s| {
|
||||
let settings = schemars::gen::SchemaSettings::default().with(|s| {
|
||||
s.option_nullable = false;
|
||||
s.option_add_null_type = false;
|
||||
s.inline_subschemas = true;
|
||||
});
|
||||
|
||||
let generator = settings.into_generator();
|
||||
let socket_message = generator.into_root_schema_for::<KomobarConfig>();
|
||||
let gen = settings.into_generator();
|
||||
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
|
||||
println!("{schema}");
|
||||
@@ -137,17 +137,13 @@ fn main() -> color_eyre::Result<()> {
|
||||
}
|
||||
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
tracing::subscriber::set_global_default(
|
||||
@@ -159,14 +155,13 @@ 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 = home_path.replace_env();
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
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");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -175,7 +170,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.display()
|
||||
home_dir.as_path().display()
|
||||
);
|
||||
|
||||
std::process::exit(0);
|
||||
@@ -183,11 +178,16 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let default_config_path = home_dir.join("komorebi.bar.json");
|
||||
|
||||
let config_path = opts.config.or_else(|| {
|
||||
default_config_path
|
||||
.is_file()
|
||||
.then_some(default_config_path.clone())
|
||||
});
|
||||
let config_path = opts.config.map_or_else(
|
||||
|| {
|
||||
if !default_config_path.is_file() {
|
||||
None
|
||||
} else {
|
||||
Some(default_config_path.clone())
|
||||
}
|
||||
},
|
||||
Option::from,
|
||||
);
|
||||
|
||||
let mut config = match config_path {
|
||||
None => {
|
||||
@@ -197,14 +197,17 @@ fn main() -> color_eyre::Result<()> {
|
||||
std::fs::write(&default_config_path, komorebi_bar_json)?;
|
||||
tracing::info!(
|
||||
"created example configuration file: {}",
|
||||
default_config_path.display()
|
||||
default_config_path.as_path().display()
|
||||
);
|
||||
|
||||
KomobarConfig::read(&default_config_path)?
|
||||
}
|
||||
Some(ref config) => {
|
||||
if !opts.aliases {
|
||||
tracing::info!("found configuration file: {}", config.display());
|
||||
tracing::info!(
|
||||
"found configuration file: {}",
|
||||
config.as_path().to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
KomobarConfig::read(config)?
|
||||
@@ -234,17 +237,17 @@ fn main() -> color_eyre::Result<()> {
|
||||
.map_or(usr_monitor_index, |i| *i);
|
||||
|
||||
MONITOR_RIGHT.store(
|
||||
state.monitors.elements()[monitor_index].size.right,
|
||||
state.monitors.elements()[monitor_index].size().right,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[monitor_index].size.top,
|
||||
state.monitors.elements()[monitor_index].size().top,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_LEFT.store(
|
||||
state.monitors.elements()[monitor_index].size.left,
|
||||
state.monitors.elements()[monitor_index].size().left,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
@@ -254,11 +257,11 @@ fn main() -> color_eyre::Result<()> {
|
||||
None => {
|
||||
config.position = Some(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size.left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size.top as f32,
|
||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
||||
}),
|
||||
end: Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size.right as f32,
|
||||
x: state.monitors.elements()[monitor_index].size().right as f32,
|
||||
y: 50.0,
|
||||
}),
|
||||
})
|
||||
@@ -266,14 +269,14 @@ fn main() -> color_eyre::Result<()> {
|
||||
Some(ref mut position) => {
|
||||
if position.start.is_none() {
|
||||
position.start = Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size.left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size.top as f32,
|
||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
||||
});
|
||||
}
|
||||
|
||||
if position.end.is_none() {
|
||||
position.end = Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size.right as f32,
|
||||
x: state.monitors.elements()[monitor_index].size().right as f32,
|
||||
y: 50.0,
|
||||
})
|
||||
}
|
||||
@@ -304,7 +307,10 @@ 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.display());
|
||||
tracing::info!(
|
||||
"configuration file updated: {}",
|
||||
config_path_cl.as_path().to_string_lossy()
|
||||
);
|
||||
|
||||
if let Err(error) = tx_config.send(updated) {
|
||||
tracing::error!("could not send configuration update to gui: {error}")
|
||||
@@ -358,7 +364,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
while komorebi_client::send_message(
|
||||
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
|
||||
)
|
||||
.is_err()
|
||||
.is_err()
|
||||
{
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
@@ -409,5 +415,5 @@ fn main() -> color_eyre::Result<()> {
|
||||
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
|
||||
}),
|
||||
)
|
||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
@@ -13,14 +11,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::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
@@ -60,10 +55,6 @@ 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 {
|
||||
@@ -117,10 +108,6 @@ 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,8 +133,6 @@ 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,29 +10,15 @@ 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,
|
||||
selected_fill: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_auto(selected: bool, selected_fill: Option<Color32>) -> Self {
|
||||
Self {
|
||||
selected,
|
||||
selected_fill,
|
||||
}
|
||||
Self { selected }
|
||||
}
|
||||
|
||||
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
|
||||
let Self {
|
||||
selected,
|
||||
selected_fill,
|
||||
} = self;
|
||||
let Self { selected } = self;
|
||||
|
||||
Frame::NONE
|
||||
.show(ui, |ui| {
|
||||
@@ -46,16 +32,7 @@ impl SelectableFrame {
|
||||
);
|
||||
|
||||
// since the stroke is drawn inside the frame, we always reserve space for it
|
||||
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() {
|
||||
if response.hovered() || response.highlighted() || response.has_focus() {
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
Frame::NONE
|
||||
@@ -70,7 +47,7 @@ impl SelectableFrame {
|
||||
Frame::NONE
|
||||
.stroke(Stroke::new(1.0, visuals.bg_fill))
|
||||
.corner_radius(visuals.corner_radius)
|
||||
.fill(selected_fill.unwrap_or(visuals.bg_fill))
|
||||
.fill(visuals.bg_fill)
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
} else {
|
||||
|
||||
@@ -1,406 +0,0 @@
|
||||
use super::ImageIcon;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Image;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::RichText;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::vec2;
|
||||
use komorebi_client::PathExt;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tracing;
|
||||
use which::which;
|
||||
|
||||
/// Minimum interval between consecutive application launches to prevent accidental spamming.
|
||||
const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ApplicationsConfig {
|
||||
/// Enables or disables the applications widget.
|
||||
pub enable: bool,
|
||||
/// Whether to show the launch command on hover (optional).
|
||||
/// Could be overridden per application. Defaults to `false` if not set.
|
||||
pub show_command_on_hover: Option<bool>,
|
||||
/// Horizontal spacing between application buttons.
|
||||
pub spacing: Option<f32>,
|
||||
/// Default display format for all applications (optional).
|
||||
/// Could be overridden per application. Defaults to `Icon`.
|
||||
pub display: Option<DisplayFormat>,
|
||||
/// List of configured applications to display.
|
||||
pub items: Vec<AppConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct AppConfig {
|
||||
/// Whether to enable this application button (optional).
|
||||
/// Inherits from the global `Applications` setting if omitted.
|
||||
pub enable: Option<bool>,
|
||||
/// Whether to show the launch command on hover (optional).
|
||||
/// Inherits from the global `Applications` setting if omitted.
|
||||
pub show_command_on_hover: Option<bool>,
|
||||
/// Display name of the application.
|
||||
pub name: String,
|
||||
/// Optional icon: a path to an image or a text-based glyph (e.g., from Nerd Fonts).
|
||||
/// If not set, and if the `command` is a path to an executable, an icon might be extracted from it.
|
||||
/// Note: glyphs require a compatible `font_family`.
|
||||
pub icon: Option<String>,
|
||||
/// Command to execute (e.g. path to the application or shell command).
|
||||
pub command: String,
|
||||
/// Display format for this application button (optional). Overrides global format if set.
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum DisplayFormat {
|
||||
/// Show only the application icon.
|
||||
#[default]
|
||||
Icon,
|
||||
/// Show only the application name as text.
|
||||
Text,
|
||||
/// Show both the application icon and name.
|
||||
IconAndText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Applications {
|
||||
/// Whether the applications widget is enabled.
|
||||
pub enable: bool,
|
||||
/// Horizontal spacing between application buttons.
|
||||
pub spacing: Option<f32>,
|
||||
/// Applications to be rendered in the UI.
|
||||
pub items: Vec<App>,
|
||||
}
|
||||
|
||||
impl BarWidget for Applications {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if !self.enable {
|
||||
return;
|
||||
}
|
||||
|
||||
let icon_config = IconConfig {
|
||||
font_id: config.icon_font_id.clone(),
|
||||
size: config.icon_font_id.size,
|
||||
color: ctx.style().visuals.selection.stroke.color,
|
||||
};
|
||||
|
||||
if let Some(spacing) = self.spacing {
|
||||
ui.spacing_mut().item_spacing.x = spacing;
|
||||
}
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for app in &mut self.items {
|
||||
app.render(ctx, ui, &icon_config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ApplicationsConfig> for Applications {
|
||||
fn from(applications_config: &ApplicationsConfig) -> Self {
|
||||
let items = applications_config
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, config)| {
|
||||
let command = UserCommand::new(&config.command);
|
||||
|
||||
App {
|
||||
enable: config.enable.unwrap_or(applications_config.enable),
|
||||
#[allow(clippy::obfuscated_if_else)]
|
||||
name: config
|
||||
.name
|
||||
.is_empty()
|
||||
.then(|| format!("App {}", index + 1))
|
||||
.unwrap_or_else(|| config.name.clone()),
|
||||
icon: Icon::try_from_path(config.icon.as_deref())
|
||||
.or_else(|| Icon::try_from_command(&command)),
|
||||
command,
|
||||
display: config
|
||||
.display
|
||||
.or(applications_config.display)
|
||||
.unwrap_or_default(),
|
||||
show_command_on_hover: config
|
||||
.show_command_on_hover
|
||||
.or(applications_config.show_command_on_hover)
|
||||
.unwrap_or(false),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
enable: applications_config.enable,
|
||||
items,
|
||||
spacing: applications_config.spacing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single resolved application entry used at runtime.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct App {
|
||||
/// Whether this application is enabled.
|
||||
pub enable: bool,
|
||||
/// Display name of the application. Defaults to "App N" if not set.
|
||||
pub name: String,
|
||||
/// Icon to display for this application, if available.
|
||||
pub icon: Option<Icon>,
|
||||
/// Command to execute when the application is launched.
|
||||
pub command: UserCommand,
|
||||
/// Display format (icon, text, or both).
|
||||
pub display: DisplayFormat,
|
||||
/// Whether to show the launch command on hover.
|
||||
pub show_command_on_hover: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Renders the application button in the provided `Ui` context with a given icon size.
|
||||
#[inline]
|
||||
pub fn render(&mut self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||
if self.enable
|
||||
&& SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = Vec2::splat(4.0);
|
||||
|
||||
match self.display {
|
||||
DisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
|
||||
DisplayFormat::Text => self.draw_name(ui),
|
||||
DisplayFormat::IconAndText => {
|
||||
self.draw_icon(ctx, ui, icon_config);
|
||||
self.draw_name(ui);
|
||||
}
|
||||
}
|
||||
|
||||
// Add hover text with command information
|
||||
let response = ui.response();
|
||||
if self.show_command_on_hover {
|
||||
response.on_hover_text(format!("Launch: {}", self.command.as_ref()));
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
// Launch the application when clicked
|
||||
self.command.launch_if_ready();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the application's icon within the UI if available,
|
||||
/// or falls back to a default placeholder icon.
|
||||
#[inline]
|
||||
fn draw_icon(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||
if let Some(icon) = &self.icon {
|
||||
icon.draw(ctx, ui, icon_config);
|
||||
} else {
|
||||
Icon::draw_fallback(ui, Vec2::splat(icon_config.size));
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays the application's name as a non-selectable label within the UI.
|
||||
#[inline]
|
||||
fn draw_name(&self, ui: &mut Ui) {
|
||||
ui.add(Label::new(&self.name).selectable(false));
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds image/text data to be used as an icon in the UI.
|
||||
/// This represents source icon data before rendering.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Icon {
|
||||
/// RGBA image used for rendering the icon.
|
||||
Image(ImageIcon),
|
||||
/// Text-based icon, e.g. from a font like Nerd Fonts.
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
/// Attempts to create an [`Icon`] from a string path or text glyph/glyphs.
|
||||
///
|
||||
/// - Environment variables in the path are resolved using [`PathExt::replace_env`].
|
||||
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved path.
|
||||
/// - If the path is invalid but the string is non-empty, it is interpreted as a text-based icon and
|
||||
/// returned as [`Icon::Text`].
|
||||
/// - Returns `None` if the input is empty, `None`, or image loading fails.
|
||||
#[inline]
|
||||
pub fn try_from_path(icon: Option<&str>) -> Option<Self> {
|
||||
let icon = icon.map(str::trim)?;
|
||||
if icon.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = icon.replace_env();
|
||||
if !path.is_file() {
|
||||
return Some(Icon::Text(icon.to_owned()));
|
||||
}
|
||||
|
||||
let image_icon = ImageIcon::try_load(path.as_ref(), || match image::open(&path) {
|
||||
Ok(img) => Some(img),
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to load icon from {:?}, error: {}", path, err);
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
Some(Icon::Image(image_icon))
|
||||
}
|
||||
|
||||
/// Attempts to create an [`Icon`] by extracting an image from the executable path of a [`UserCommand`].
|
||||
///
|
||||
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved executable path.
|
||||
/// - Returns [`Icon::Image`] if an icon is successfully extracted.
|
||||
/// - Returns `None` if the executable path is unavailable or icon extraction fails.
|
||||
#[inline]
|
||||
pub fn try_from_command(command: &UserCommand) -> Option<Self> {
|
||||
let path = command.get_executable()?;
|
||||
let image_icon = ImageIcon::try_load(path.as_ref(), || {
|
||||
let path_str = path.to_str()?;
|
||||
windows_icons::get_icon_by_path(path_str)
|
||||
.or_else(|| windows_icons_fallback::get_icon_by_path(path_str))
|
||||
})?;
|
||||
Some(Icon::Image(image_icon))
|
||||
}
|
||||
|
||||
/// Renders the icon in the given [`Ui`] using the provided [`IconConfig`].
|
||||
#[inline]
|
||||
pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||
match self {
|
||||
Icon::Image(image_icon) => {
|
||||
Frame::NONE
|
||||
.inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Image::from_texture(&image_icon.texture(ctx))
|
||||
.maintain_aspect_ratio(true)
|
||||
.fit_to_exact_size(Vec2::splat(icon_config.size)),
|
||||
);
|
||||
});
|
||||
}
|
||||
Icon::Text(icon) => {
|
||||
let rich_text = RichText::new(icon)
|
||||
.font(icon_config.font_id.clone())
|
||||
.size(icon_config.size)
|
||||
.color(icon_config.color);
|
||||
ui.add(Label::new(rich_text).selectable(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a fallback icon when the specified icon cannot be loaded.
|
||||
/// Displays a simple crossed-out rectangle as a placeholder.
|
||||
#[inline]
|
||||
pub fn draw_fallback(ui: &mut Ui, icon_size: Vec2) {
|
||||
let (response, painter) = ui.allocate_painter(icon_size, Sense::hover());
|
||||
let stroke = Stroke::new(1.0, ui.style().visuals.text_color());
|
||||
let mut rect = response.rect;
|
||||
let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
|
||||
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration structure for icon rendering
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IconConfig {
|
||||
/// Font used for text-based icons
|
||||
pub font_id: FontId,
|
||||
/// Size of the icon
|
||||
pub size: f32,
|
||||
/// Color of the icon used for text-based icons
|
||||
pub color: Color32,
|
||||
}
|
||||
|
||||
/// A structure to manage command execution with cooldown prevention.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserCommand {
|
||||
/// The command string to execute
|
||||
pub command: Arc<str>,
|
||||
/// Last time this command was executed (used for cooldown control)
|
||||
pub last_launch: Instant,
|
||||
}
|
||||
|
||||
impl AsRef<str> for UserCommand {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.command
|
||||
}
|
||||
}
|
||||
|
||||
impl UserCommand {
|
||||
/// Creates a new [`UserCommand`] with environment variables in the command path
|
||||
/// resolved using [`PathExt::replace_env`].
|
||||
#[inline]
|
||||
pub fn new(command: &str) -> Self {
|
||||
// Allow immediate launch by initializing last_launch in the past
|
||||
let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL;
|
||||
|
||||
Self {
|
||||
command: Arc::from(command.replace_env().to_str().unwrap_or_default()),
|
||||
last_launch,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to resolve the executable path from the command string.
|
||||
///
|
||||
/// Resolution logic:
|
||||
/// - Splits the command by ".exe" and checks if the first part is an existing file.
|
||||
/// - If not, attempts to locate the binary using [`which`] on this name.
|
||||
/// - If still unresolved, takes the first word (separated by whitespace) and attempts
|
||||
/// to find it in the system `PATH` using [`which`].
|
||||
///
|
||||
/// Returns `None` if no executable path can be determined.
|
||||
#[inline]
|
||||
pub fn get_executable(&self) -> Option<Cow<'_, Path>> {
|
||||
if let Some(binary) = self.command.split(".exe").next().map(Path::new) {
|
||||
if binary.is_file() {
|
||||
return Some(Cow::Borrowed(binary));
|
||||
} else if let Ok(binary) = which(binary) {
|
||||
return Some(Cow::Owned(binary));
|
||||
}
|
||||
}
|
||||
|
||||
which(self.command.split(' ').next()?).ok().map(Cow::Owned)
|
||||
}
|
||||
|
||||
/// Attempts to launch the specified command in a separate thread if enough time has passed
|
||||
/// since the last launch. This prevents repeated launches from rapid consecutive clicks.
|
||||
///
|
||||
/// Errors during launch are logged using the `tracing` crate.
|
||||
pub fn launch_if_ready(&mut self) {
|
||||
let now = Instant::now();
|
||||
// Check if enough time has passed since the last launch
|
||||
if now.duration_since(self.last_launch) < MIN_LAUNCH_INTERVAL {
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_launch = now;
|
||||
let command_string = self.command.clone();
|
||||
// Launch the application in a separate thread to avoid blocking the UI
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = Command::new("cmd").args(["/C", &command_string]).spawn() {
|
||||
tracing::error!("Failed to launch command '{}': {}", command_string, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,17 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use starship_battery::units::ratio::percent;
|
||||
use starship_battery::Manager;
|
||||
use starship_battery::State;
|
||||
use starship_battery::units::ratio::percent;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
@@ -28,8 +28,6 @@ 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 {
|
||||
@@ -40,10 +38,9 @@ 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: None,
|
||||
last_state: String::new(),
|
||||
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))
|
||||
@@ -55,16 +52,6 @@ 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 {
|
||||
@@ -74,54 +61,38 @@ pub struct Battery {
|
||||
pub state: BatteryState,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select_under: Option<u8>,
|
||||
last_state: Option<BatteryOutput>,
|
||||
last_state: String,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Battery {
|
||||
fn output(&mut self) -> Option<BatteryOutput> {
|
||||
fn output(&mut self) -> String {
|
||||
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 = None;
|
||||
output.clear();
|
||||
|
||||
if let Ok(mut batteries) = self.manager.batteries()
|
||||
&& let Some(Ok(first)) = batteries.nth(0)
|
||||
{
|
||||
let percentage = first.state_of_charge().get::<percent>().round() as u8;
|
||||
if let Ok(mut batteries) = self.manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>();
|
||||
|
||||
if percentage == 100 && self.hide_on_full_charge {
|
||||
output = None
|
||||
} else {
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => {
|
||||
self.state = match percentage {
|
||||
p if p > 75 => BatteryState::Discharging,
|
||||
p if p > 50 => BatteryState::High,
|
||||
p if p > 25 => BatteryState::Medium,
|
||||
p if p > 10 => BatteryState::Low,
|
||||
_ => BatteryState::Warning,
|
||||
}
|
||||
if percentage == 100.0 && self.hide_on_full_charge {
|
||||
output = String::new()
|
||||
} else {
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => self.state = BatteryState::Discharging,
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
|
||||
|
||||
output = Some(BatteryOutput {
|
||||
label: match self.label_prefix {
|
||||
output = match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage}%")
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => {
|
||||
format!("{percentage}%")
|
||||
}
|
||||
},
|
||||
selected,
|
||||
})
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,50 +108,44 @@ impl BarWidget for Battery {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if let Some(output) = output {
|
||||
if !output.is_empty() {
|
||||
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(),
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output.label,
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
color: 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_auto(output.selected, auto_focus_fill)
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
&& let Err(error) = Command::new("cmd.exe")
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args(["/C", "start", "ms-settings:batterysaver"])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{error}")
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -25,8 +25,6 @@ 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 {
|
||||
@@ -40,7 +38,6 @@ 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(),
|
||||
@@ -48,38 +45,26 @@ 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) -> CpuOutput {
|
||||
fn output(&mut self) -> String {
|
||||
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() 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,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,9 +73,7 @@ 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.label.is_empty() {
|
||||
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
|
||||
|
||||
if !output.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
@@ -99,31 +82,31 @@ impl BarWidget for Cpu {
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output.label,
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
color: 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_auto(output.selected, auto_focus_fill)
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
&& let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{error}")
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,16 +4,15 @@ use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -167,7 +166,7 @@ impl Date {
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
Err(_) => format!("Invalid timezone: {timezone}"),
|
||||
Err(_) => format!("Invalid timezone: {}", timezone),
|
||||
},
|
||||
None => Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
@@ -226,7 +225,7 @@ impl BarWidget for Date {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use color_eyre::eyre;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows::Win32::Globalization::LCIDToLocaleName;
|
||||
@@ -82,7 +80,7 @@ pub struct Keyboard {
|
||||
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
|
||||
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
|
||||
/// invalid UTF-16 characters during conversion.
|
||||
fn get_active_keyboard_layout() -> eyre::Result<String, ()> {
|
||||
fn get_active_keyboard_layout() -> Result<String, ()> {
|
||||
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
|
||||
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
|
||||
|
||||
@@ -171,10 +169,7 @@ impl BarWidget for Keyboard {
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
|
||||
.selectable(false),
|
||||
)
|
||||
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ use crate::config::DisplayFormat;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::komorebi::KomorebiLayoutConfig;
|
||||
use color_eyre::eyre;
|
||||
use eframe::egui::vec2;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
@@ -13,12 +13,11 @@ use eframe::egui::Stroke;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::vec2;
|
||||
use komorebi_client::SocketMessage;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::de::Error;
|
||||
use serde_json::from_str;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
@@ -35,14 +34,15 @@ pub enum KomorebiLayout {
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
fn deserialize<D>(deserializer: D) -> eyre::Result<Self, D::Error>
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: String = String::deserialize(deserializer)?;
|
||||
|
||||
// Attempt to deserialize the string as a DefaultLayout
|
||||
if let Ok(default_layout) = from_str::<komorebi_client::DefaultLayout>(&format!("\"{s}\""))
|
||||
if let Ok(default_layout) =
|
||||
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
|
||||
{
|
||||
return Ok(KomorebiLayout::Default(default_layout));
|
||||
}
|
||||
@@ -53,7 +53,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
"Floating" => Ok(KomorebiLayout::Floating),
|
||||
"Paused" => Ok(KomorebiLayout::Paused),
|
||||
"Custom" => Ok(KomorebiLayout::Custom),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,15 +92,16 @@ impl KomorebiLayout {
|
||||
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
|
||||
match self {
|
||||
KomorebiLayout::Default(option) => {
|
||||
if let Some(ws_idx) = workspace_idx
|
||||
&& komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
||||
if let Some(ws_idx) = workspace_idx {
|
||||
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
||||
monitor_idx,
|
||||
ws_idx,
|
||||
*option,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
||||
}
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Monocle => {
|
||||
@@ -187,12 +188,6 @@ 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 => {
|
||||
@@ -256,7 +251,7 @@ impl KomorebiLayout {
|
||||
let layout_frame = SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
|
||||
self.show_icon(true, font_id.clone(), ctx, ui);
|
||||
self.show_icon(false, font_id.clone(), ctx, ui);
|
||||
}
|
||||
|
||||
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
|
||||
@@ -269,53 +264,57 @@ impl KomorebiLayout {
|
||||
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
|
||||
}
|
||||
|
||||
if show_options && let Some(workspace_idx) = workspace_idx {
|
||||
Frame::NONE.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||
.selectable(false),
|
||||
);
|
||||
if show_options {
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
Frame::NONE.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||
.selectable(false),
|
||||
);
|
||||
|
||||
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::HorizontalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
||||
//KomorebiLayout::Custom,
|
||||
KomorebiLayout::Monocle,
|
||||
KomorebiLayout::Floating,
|
||||
KomorebiLayout::Paused,
|
||||
]);
|
||||
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::HorizontalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
||||
//KomorebiLayout::Custom,
|
||||
KomorebiLayout::Monocle,
|
||||
KomorebiLayout::Floating,
|
||||
KomorebiLayout::Paused,
|
||||
]);
|
||||
|
||||
for layout_option in &mut layout_options {
|
||||
let is_selected = self == layout_option;
|
||||
for layout_option in &mut layout_options {
|
||||
let is_selected = self == layout_option;
|
||||
|
||||
if SelectableFrame::new(is_selected)
|
||||
.show(ui, |ui| {
|
||||
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
|
||||
})
|
||||
.on_hover_text(match layout_option {
|
||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
||||
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
||||
KomorebiLayout::Custom => "Custom".to_string(),
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
||||
show_options = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
if SelectableFrame::new(is_selected)
|
||||
.show(ui, |ui| {
|
||||
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
|
||||
})
|
||||
.on_hover_text(match layout_option {
|
||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
||||
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
||||
KomorebiLayout::Custom => "Custom".to_string(),
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
||||
show_options = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -40,34 +40,36 @@ impl Media {
|
||||
enable,
|
||||
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
|
||||
.unwrap()
|
||||
.join()
|
||||
.get()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(op) = session.TryTogglePlayPauseAsync()
|
||||
{
|
||||
op.join().unwrap_or_default();
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
||||
if let Ok(op) = session.TryTogglePlayPauseAsync() {
|
||||
op.get().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn output(&mut self) -> String {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
|
||||
&& let Ok(properties) = operation.join()
|
||||
&& let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title())
|
||||
{
|
||||
if artist.is_empty() {
|
||||
return format!("{title}");
|
||||
}
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
||||
if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
|
||||
if let Ok(properties) = operation.get() {
|
||||
if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
|
||||
if artist.is_empty() {
|
||||
return format!("{title}");
|
||||
}
|
||||
|
||||
if title.is_empty() {
|
||||
return format!("{artist}");
|
||||
}
|
||||
if title.is_empty() {
|
||||
return format!("{artist}");
|
||||
}
|
||||
|
||||
return format!("{artist} - {title}");
|
||||
return format!("{artist} - {title}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String::new()
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -25,8 +25,6 @@ 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 {
|
||||
@@ -40,7 +38,6 @@ 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(),
|
||||
@@ -48,23 +45,16 @@ 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) -> MemoryOutput {
|
||||
fn output(&mut self) -> String {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.system.refresh_memory();
|
||||
@@ -73,17 +63,11 @@ impl Memory {
|
||||
|
||||
let used = self.system.used_memory();
|
||||
let total = self.system.total_memory();
|
||||
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,
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,9 +76,7 @@ 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.label.is_empty() {
|
||||
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
|
||||
|
||||
if !output.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
@@ -103,31 +85,31 @@ impl BarWidget for Memory {
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output.label,
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
color: 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_auto(output.selected, auto_focus_fill)
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
&& let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{error}")
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
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;
|
||||
@@ -23,151 +11,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
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 eframe::egui::text::LayoutJob;
|
||||
use num_derive::FromPrimitive;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -24,58 +22,36 @@ use sysinfo::Networks;
|
||||
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 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 received and transmitted activity
|
||||
#[serde(alias = "network_activity_fill_characters")]
|
||||
pub activity_left_padding: Option<usize>,
|
||||
/// 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>,
|
||||
/// 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 default_refresh_interval = 10;
|
||||
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_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(),
|
||||
default_refresh_interval,
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
auto_select: value.auto_select,
|
||||
activity_left_padding: value.activity_left_padding.unwrap_or_default(),
|
||||
last_updated_default_interface: Instant::now()
|
||||
.checked_sub(Duration::from_secs(default_refresh_interval))
|
||||
.unwrap(),
|
||||
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()
|
||||
@@ -91,32 +67,21 @@ pub struct Network {
|
||||
pub show_activity: bool,
|
||||
pub show_default_interface: bool,
|
||||
networks_network_activity: Networks,
|
||||
default_refresh_interval: u64,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select: Option<NetworkSelectConfig>,
|
||||
default_interface: String,
|
||||
last_updated_default_interface: Instant,
|
||||
last_state_total_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
last_updated_network_activity: Instant,
|
||||
activity_left_padding: usize,
|
||||
network_activity_fill_characters: usize,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
fn default_interface(&mut self) {
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated_default_interface)
|
||||
> Duration::from_secs(self.default_refresh_interval)
|
||||
{
|
||||
if let Ok(interface) = netdev::get_default_interface()
|
||||
&& let Some(friendly_name) = &interface.friendly_name
|
||||
{
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
}
|
||||
|
||||
self.last_updated_default_interface = now;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,40 +96,35 @@ impl Network {
|
||||
activity.clear();
|
||||
total_activity.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface()
|
||||
&& let Some(friendly_name) = &interface.friendly_name
|
||||
{
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
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);
|
||||
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,
|
||||
);
|
||||
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,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
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),
|
||||
))
|
||||
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),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,121 +138,105 @@ impl Network {
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
fn reading_to_labels(
|
||||
fn reading_to_label(
|
||||
&self,
|
||||
select_received: bool,
|
||||
select_transmitted: bool,
|
||||
ctx: &Context,
|
||||
reading: &NetworkReading,
|
||||
reading: NetworkReading,
|
||||
config: RenderConfig,
|
||||
) -> (Label, 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
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"{: >width$}/s",
|
||||
reading.transmitted.pretty,
|
||||
width = self.activity_left_padding
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("{} ", reading.received.pretty),
|
||||
reading.transmitted.pretty.clone(),
|
||||
format!("{} ", reading.received_text),
|
||||
reading.transmitted_text,
|
||||
),
|
||||
},
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"DOWN: {: >width$}/s ",
|
||||
reading.received.pretty,
|
||||
width = self.activity_left_padding
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"UP: {: >width$}/s",
|
||||
reading.transmitted.pretty,
|
||||
width = self.activity_left_padding
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("\u{2211}DOWN: {}/s ", reading.received.pretty),
|
||||
format!("\u{2211}UP: {}/s", reading.transmitted.pretty),
|
||||
format!("\u{2211}DOWN: {}/s ", reading.received_text),
|
||||
format!("\u{2211}UP: {}/s", reading.transmitted_text),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
let auto_text_color_received = config.auto_select_text.filter(|_| select_received);
|
||||
let auto_text_color_transmitted = config.auto_select_text.filter(|_| select_transmitted);
|
||||
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_down = LayoutJob::simple(
|
||||
let mut layout_job = 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()
|
||||
}
|
||||
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),
|
||||
icon_format.font_id.clone(),
|
||||
icon_format.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job_down.append(
|
||||
layout_job.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()
|
||||
},
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
// icon
|
||||
let mut layout_job_up = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
layout_job.append(
|
||||
&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()
|
||||
}
|
||||
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,
|
||||
0.0,
|
||||
icon_format.clone(),
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job_up.append(
|
||||
layout_job.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()
|
||||
},
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
(
|
||||
Label::new(layout_job_down).selectable(false),
|
||||
Label::new(layout_job_up).selectable(false),
|
||||
)
|
||||
Label::new(layout_job).selectable(false)
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> (u64, String) {
|
||||
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;
|
||||
|
||||
@@ -304,29 +248,10 @@ impl Network {
|
||||
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()
|
||||
&& let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
|
||||
{
|
||||
eprintln!("{error}");
|
||||
match base {
|
||||
Some(DataUnit::B) => format!("{result:.1} B"),
|
||||
Some(unit) => format!("{result:.1} {unit}iB"),
|
||||
None => String::from("Unknown data unit"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,8 +259,6 @@ impl Network {
|
||||
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();
|
||||
|
||||
@@ -343,102 +266,17 @@ impl BarWidget for Network {
|
||||
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),
|
||||
);
|
||||
}
|
||||
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(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),
|
||||
);
|
||||
}
|
||||
for reading in activity {
|
||||
render_config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading, config.clone()));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -476,9 +314,15 @@ impl BarWidget for Network {
|
||||
);
|
||||
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
self.show_frame(false, None, ui, |ui| {
|
||||
ui.add(Label::new(layout_job).selectable(false))
|
||||
});
|
||||
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)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -495,38 +339,19 @@ enum NetworkReadingFormat {
|
||||
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,
|
||||
pub format: NetworkReadingFormat,
|
||||
pub received_text: String,
|
||||
pub transmitted_text: String,
|
||||
}
|
||||
|
||||
impl NetworkReading {
|
||||
fn new(
|
||||
format: NetworkReadingFormat,
|
||||
received: ReadingValue,
|
||||
transmitted: ReadingValue,
|
||||
) -> Self {
|
||||
Self {
|
||||
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
|
||||
NetworkReading {
|
||||
format,
|
||||
received,
|
||||
transmitted,
|
||||
received_text: received,
|
||||
transmitted_text: transmitted,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,6 +371,6 @@ enum DataUnit {
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
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::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -25,14 +24,6 @@ pub struct StorageConfig {
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Show disks that are read only. (default: false)
|
||||
pub show_read_only_disks: Option<bool>,
|
||||
/// Show removable disks. (default: true)
|
||||
pub show_removable_disks: Option<bool>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
/// Hide when the current percentage is under this value [[1-100]]
|
||||
pub auto_hide_under: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<StorageConfig> for Storage {
|
||||
@@ -42,34 +33,21 @@ impl From<StorageConfig> for Storage {
|
||||
disks: Disks::new_with_refreshed_list(),
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
|
||||
show_removable_disks: value.show_removable_disks.unwrap_or(true),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StorageDisk {
|
||||
label: String,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
pub struct Storage {
|
||||
pub enable: bool,
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
show_read_only_disks: bool,
|
||||
show_removable_disks: bool,
|
||||
auto_select_over: Option<u8>,
|
||||
auto_hide_under: Option<u8>,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
fn output(&mut self) -> Vec<StorageDisk> {
|
||||
fn output(&mut self) -> Vec<String> {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.disks.refresh(true);
|
||||
@@ -79,36 +57,21 @@ impl Storage {
|
||||
let mut disks = vec![];
|
||||
|
||||
for disk in &self.disks {
|
||||
if disk.is_read_only() && !self.show_read_only_disks {
|
||||
continue;
|
||||
}
|
||||
if disk.is_removable() && !self.show_removable_disks {
|
||||
continue;
|
||||
}
|
||||
let mount = disk.mount_point();
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
let used = total - available;
|
||||
let percentage = ((used * 100) / total) as u8;
|
||||
|
||||
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.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),
|
||||
})
|
||||
}
|
||||
|
||||
disks.sort_by(|a, b| a.label.cmp(&b.label));
|
||||
disks.sort();
|
||||
disks.reverse();
|
||||
|
||||
disks
|
||||
}
|
||||
@@ -117,16 +80,7 @@ impl Storage {
|
||||
impl BarWidget for Storage {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
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);
|
||||
|
||||
for output in self.output() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
@@ -135,36 +89,36 @@ impl BarWidget for Storage {
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output.label,
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
|
||||
color: 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_auto(output.selected, auto_focus_fill)
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
&& let Err(error) = Command::new("cmd.exe")
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.label.split(' ').collect::<Vec<&str>>()[0],
|
||||
output.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{error}")
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::widgets::widget::BarWidget;
|
||||
use chrono::Local;
|
||||
use chrono::NaiveTime;
|
||||
use chrono_tz::Tz;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
@@ -15,7 +16,6 @@ use eframe::egui::Stroke;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::epaint::StrokeKind;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
@@ -209,7 +209,7 @@ impl Time {
|
||||
Some(dt.time()),
|
||||
)
|
||||
}
|
||||
Err(_) => (format!("Invalid timezone: {timezone:?}"), None),
|
||||
Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
|
||||
},
|
||||
None => {
|
||||
let dt = Local::now();
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -140,14 +140,16 @@ impl BarWidget for Update {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
&& let Err(error) = Command::new("explorer.exe")
|
||||
{
|
||||
if let Err(error) = Command::new("explorer.exe")
|
||||
.args([format!(
|
||||
"https://github.com/LGUG2Z/komorebi/releases/v{}",
|
||||
self.latest_version
|
||||
)])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{error}")
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widgets::applications::Applications;
|
||||
use crate::widgets::applications::ApplicationsConfig;
|
||||
use crate::widgets::battery::Battery;
|
||||
use crate::widgets::battery::BatteryConfig;
|
||||
use crate::widgets::cpu::Cpu;
|
||||
@@ -35,7 +33,6 @@ 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),
|
||||
@@ -52,7 +49,6 @@ 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())),
|
||||
@@ -69,7 +65,6 @@ 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,
|
||||
@@ -77,7 +72,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_container.as_ref().is_some_and(|w| w.enable)
|
||||
|| config.focused_window.as_ref().is_some_and(|w| w.enable)
|
||||
|| config
|
||||
.configuration_switcher
|
||||
.as_ref()
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.39"
|
||||
edition = "2024"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi = { path = "../komorebi", default-features = false }
|
||||
komorebi = { path = "../komorebi" }
|
||||
|
||||
uds_windows = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["komorebi/default"]
|
||||
schemars = ["komorebi/schemars"]
|
||||
|
||||
@@ -1,39 +1,20 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::AnimationsConfig;
|
||||
pub use komorebi::AppSpecificConfigurationPath;
|
||||
pub use komorebi::AspectRatio;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::Colour;
|
||||
pub use komorebi::CrossBoundaryBehaviour;
|
||||
pub use komorebi::GlobalState;
|
||||
pub use komorebi::KomorebiTheme;
|
||||
pub use komorebi::MonitorConfig;
|
||||
pub use komorebi::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::PredefinedAspectRatio;
|
||||
pub use komorebi::Rgb;
|
||||
pub use komorebi::RuleDebug;
|
||||
pub use komorebi::StackbarConfig;
|
||||
pub use komorebi::State;
|
||||
pub use komorebi::StaticConfig;
|
||||
pub use komorebi::SubscribeOptions;
|
||||
pub use komorebi::TabsConfig;
|
||||
pub use komorebi::VirtualDesktopNotification;
|
||||
pub use komorebi::WindowContainerBehaviour;
|
||||
pub use komorebi::WindowsApi;
|
||||
pub use komorebi::WorkspaceConfig;
|
||||
pub use komorebi::animation::PerAnimationPrefixConfig;
|
||||
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;
|
||||
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::AnimationStyle;
|
||||
pub use komorebi::core::ApplicationIdentifier;
|
||||
pub use komorebi::core::Arrangement;
|
||||
@@ -63,8 +44,6 @@ pub use komorebi::core::StackbarLabel;
|
||||
pub use komorebi::core::StackbarMode;
|
||||
pub use komorebi::core::StateQuery;
|
||||
pub use komorebi::core::WindowKind;
|
||||
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
||||
pub use komorebi::core::replace_env_in_path;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
pub use komorebi::monitor_reconciliator::MonitorNotification;
|
||||
pub use komorebi::ring::Ring;
|
||||
@@ -74,10 +53,29 @@ 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::CrossBoundaryBehaviour;
|
||||
pub use komorebi::GlobalState;
|
||||
pub use komorebi::KomorebiTheme;
|
||||
pub use komorebi::MonitorConfig;
|
||||
pub use komorebi::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::PredefinedAspectRatio;
|
||||
pub use komorebi::RuleDebug;
|
||||
pub use komorebi::StackbarConfig;
|
||||
pub use komorebi::State;
|
||||
pub use komorebi::StaticConfig;
|
||||
pub use komorebi::SubscribeOptions;
|
||||
pub use komorebi::TabsConfig;
|
||||
pub use komorebi::WindowContainerBehaviour;
|
||||
pub use komorebi::WindowsApi;
|
||||
pub use komorebi::WorkspaceConfig;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
@@ -95,15 +93,12 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()>
|
||||
where
|
||||
Q: Borrow<SocketMessage>,
|
||||
{
|
||||
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
|
||||
if let Ok(m_str) = serde_json::to_string(m.borrow()) {
|
||||
if let Ok(m_str) = serde_json::to_string(&m) {
|
||||
s.push_str(&m_str);
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.39"
|
||||
edition = "2024"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
use eframe::egui;
|
||||
use eframe::egui::color_picker::Alpha;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use eframe::egui::color_picker::Alpha;
|
||||
use komorebi_client::BorderStyle;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::DefaultLayout;
|
||||
@@ -41,9 +41,7 @@ struct BorderColours {
|
||||
single: Color32,
|
||||
stack: Color32,
|
||||
monocle: Color32,
|
||||
floating: Color32,
|
||||
unfocused: Color32,
|
||||
unfocused_locked: Color32,
|
||||
}
|
||||
|
||||
struct BorderConfig {
|
||||
@@ -78,8 +76,8 @@ impl From<&komorebi_client::Monitor> for MonitorConfig {
|
||||
}
|
||||
|
||||
Self {
|
||||
size: value.size,
|
||||
work_area_offset: value.work_area_offset.unwrap_or_default(),
|
||||
size: *value.size(),
|
||||
work_area_offset: value.work_area_offset().unwrap_or_default(),
|
||||
workspaces,
|
||||
}
|
||||
}
|
||||
@@ -95,22 +93,22 @@ struct WorkspaceConfig {
|
||||
|
||||
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
|
||||
fn from(value: &komorebi_client::Workspace) -> Self {
|
||||
let layout = match value.layout {
|
||||
Layout::Default(layout) => layout,
|
||||
let layout = match value.layout() {
|
||||
Layout::Default(layout) => *layout,
|
||||
Layout::Custom(_) => DefaultLayout::BSP,
|
||||
};
|
||||
|
||||
let name = value
|
||||
.name
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
|
||||
|
||||
Self {
|
||||
layout,
|
||||
name,
|
||||
tile: value.tile,
|
||||
workspace_padding: value.workspace_padding.unwrap_or(20),
|
||||
container_padding: value.container_padding.unwrap_or(20),
|
||||
tile: *value.tile(),
|
||||
workspace_padding: value.workspace_padding().unwrap_or(20),
|
||||
container_padding: value.container_padding().unwrap_or(20),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,9 +154,7 @@ 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 {
|
||||
@@ -381,22 +377,6 @@ 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,
|
||||
@@ -411,22 +391,6 @@ 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();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -437,7 +401,7 @@ impl eframe::App for KomorebiGui {
|
||||
BorderStyle::Square,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::Button::selectable(
|
||||
.add(egui::SelectableLabel::new(
|
||||
self.border_config.border_style == option,
|
||||
option.to_string(),
|
||||
))
|
||||
@@ -494,7 +458,7 @@ impl eframe::App for KomorebiGui {
|
||||
StackbarMode::Always,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::Button::selectable(
|
||||
.add(egui::SelectableLabel::new(
|
||||
self.stackbar_config.mode == option,
|
||||
option.to_string(),
|
||||
))
|
||||
@@ -513,7 +477,7 @@ impl eframe::App for KomorebiGui {
|
||||
ui.collapsing("Label", |ui| {
|
||||
for option in [StackbarLabel::Process, StackbarLabel::Title] {
|
||||
if ui
|
||||
.add(egui::Button::selectable(
|
||||
.add(egui::SelectableLabel::new(
|
||||
self.stackbar_config.label == option,
|
||||
option.to_string(),
|
||||
))
|
||||
@@ -772,7 +736,7 @@ impl eframe::App for KomorebiGui {
|
||||
DefaultLayout::Grid,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::Button::selectable(
|
||||
.add(egui::SelectableLabel::new(
|
||||
workspace.layout == option,
|
||||
option.to_string(),
|
||||
))
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[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 }
|
||||
@@ -1,98 +0,0 @@
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use std::path::PathBuf;
|
||||
use whkd_core::Whkdrc;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Quicklook {
|
||||
whkdrc: Option<Whkdrc>,
|
||||
filter: String,
|
||||
}
|
||||
|
||||
impl Quicklook {
|
||||
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
|
||||
// Restore app state using cc.storage (requires the "persistence" feature).
|
||||
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
|
||||
// for e.g. egui::PaintCallback.
|
||||
let mut home = std::env::var("WHKD_CONFIG_HOME").map_or_else(
|
||||
|_| {
|
||||
dirs::home_dir()
|
||||
.expect("no home directory found")
|
||||
.join(".config")
|
||||
},
|
||||
|home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!(
|
||||
"$Env:WHKD_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
home.push("whkdrc");
|
||||
|
||||
Self {
|
||||
whkdrc: whkd_parser::load(&home).ok(),
|
||||
filter: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Quicklook {
|
||||
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
|
||||
eframe::egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.set_max_width(ui.available_width());
|
||||
ui.set_max_height(ui.available_height());
|
||||
eframe::egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
eframe::egui::Grid::new("grid")
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.spacing([40.0, 4.0])
|
||||
.min_col_width(ui.available_width() / 2.0 - 20.0)
|
||||
.show(ui, |ui| {
|
||||
if let Some(whkdrc) = &self.whkdrc {
|
||||
ui.label("Filter");
|
||||
ui.add(
|
||||
eframe::egui::text_edit::TextEdit::singleline(&mut self.filter)
|
||||
.hint_text("Filter by command...")
|
||||
.background_color(ctx.style().visuals.faint_bg_color),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
for binding in &whkdrc.bindings {
|
||||
let keys = binding.keys.join(" + ");
|
||||
if self.filter.is_empty() || binding.command.contains(&self.filter)
|
||||
{
|
||||
ui.label(keys);
|
||||
ui.label(&binding.command);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let viewport_builder = ViewportBuilder::default()
|
||||
.with_resizable(true)
|
||||
.with_decorations(false);
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: viewport_builder,
|
||||
centered: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"komorebi-shortcuts",
|
||||
native_options,
|
||||
Box::new(|cc| Ok(Box::new(Quicklook::new(cc)))),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1,19 +1,14 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.39"
|
||||
edition = "2024"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "c9008bd5cfa288c926e9ea3aa18c92073f9281bd" }
|
||||
catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "96f26c88d83781f234d42222293ec73d23a39ad8" }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "bdaff30959512c4f7ee7304117076a48633d777f", default-features = false, features = ["egui31"] }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
schemars = { workspace = 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,77 +0,0 @@
|
||||
use crate::Base16ColourPalette;
|
||||
use crate::colour::Colour;
|
||||
use crate::colour::Hex;
|
||||
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,32 +1,18 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub mod colour;
|
||||
mod generator;
|
||||
|
||||
pub use generator::ThemeVariant;
|
||||
pub use generator::generate_base16_palette;
|
||||
|
||||
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;
|
||||
pub use eframe::egui::Color32;
|
||||
use eframe::egui::Shadow;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::Visuals;
|
||||
use eframe::egui::style::Selection;
|
||||
use eframe::egui::style::WidgetVisuals;
|
||||
use eframe::egui::style::Widgets;
|
||||
use serde_variant::to_variant_name;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Theme {
|
||||
/// A theme from catppuccin-egui
|
||||
@@ -39,140 +25,6 @@ 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 {
|
||||
@@ -193,7 +45,6 @@ impl Theme {
|
||||
.to_string()
|
||||
})
|
||||
.collect(),
|
||||
Theme::Custom { .. } => vec!["Custom".to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,50 +70,25 @@ pub enum Base16Value {
|
||||
Base0F,
|
||||
}
|
||||
|
||||
pub enum Base16Wrapper {
|
||||
Base16(Base16),
|
||||
Custom(Box<Base16ColourPalette>),
|
||||
}
|
||||
|
||||
impl Base16Value {
|
||||
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(),
|
||||
},
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.39"
|
||||
version = "0.1.36"
|
||||
description = "A tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -17,15 +17,17 @@ 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 = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
paste = { workspace = true }
|
||||
powershell_script = "1.0"
|
||||
regex = "1"
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
@@ -47,7 +49,6 @@ 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 }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::prefix::AnimationPrefix;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::Result;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -6,10 +6,10 @@ use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::RenderDispatcher;
|
||||
use super::ANIMATION_DURATION_GLOBAL;
|
||||
use super::ANIMATION_FPS;
|
||||
use super::ANIMATION_MANAGER;
|
||||
use super::RenderDispatcher;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
@@ -55,9 +55,9 @@ impl AnimationEngine {
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
render_dispatcher: impl RenderDispatcher + Send + 'static,
|
||||
render_dispatcher: (impl RenderDispatcher + Send + 'static),
|
||||
duration: Duration,
|
||||
) -> eyre::Result<()> {
|
||||
) -> Result<()> {
|
||||
std::thread::spawn(move || {
|
||||
let animation_key = render_dispatcher.get_animation_key();
|
||||
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::AnimationStyle;
|
||||
use crate::core::Rect;
|
||||
use crate::AnimationStyle;
|
||||
|
||||
use super::style::apply_ease_func;
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ use crate::core::animation::AnimationStyle;
|
||||
use lazy_static::lazy_static;
|
||||
use prefix::AnimationPrefix;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
|
||||
@@ -17,5 +17,5 @@ pub enum AnimationPrefix {
|
||||
}
|
||||
|
||||
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
|
||||
format!("{prefix}:{key}")
|
||||
format!("{}:{}", prefix, key)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::Result;
|
||||
|
||||
pub trait RenderDispatcher {
|
||||
fn get_animation_key(&self) -> String;
|
||||
fn pre_render(&self) -> eyre::Result<()>;
|
||||
fn render(&self, delta: f64) -> eyre::Result<()>;
|
||||
fn post_render(&self) -> eyre::Result<()>;
|
||||
fn pre_render(&self) -> Result<()>;
|
||||
fn render(&self, delta: f64) -> Result<()>;
|
||||
fn post_render(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -355,61 +355,6 @@ 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),
|
||||
@@ -442,6 +387,5 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t),
|
||||
AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
|
||||
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
|
||||
AnimationStyle::CubicBezier(x1, y1, x2, y2) => CubicBezier { x1, y1, x2, y2 }.evaluate(t),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
use crate::WINDOWS_11;
|
||||
use crate::WindowsApi;
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::border_manager::RenderTarget;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::RenderTarget;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Foundation::FALSE;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::TRUE;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
|
||||
@@ -33,34 +38,31 @@ use windows::Win32::Graphics::Direct2D::D2D1_PRESENT_OPTIONS_IMMEDIATELY;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
||||
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
|
||||
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
|
||||
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
|
||||
use windows::Win32::Graphics::Gdi::CreateRectRgn;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::ValidateRect;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::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;
|
||||
@@ -102,10 +104,10 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||
let hwnd = hwnd.0 as isize;
|
||||
|
||||
if let Ok(class) = WindowsApi::real_window_class_w(hwnd)
|
||||
&& class.starts_with("komoborder")
|
||||
{
|
||||
hwnds.push(hwnd);
|
||||
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
|
||||
if class.starts_with("komoborder") {
|
||||
hwnds.push(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
true.into()
|
||||
@@ -116,7 +118,7 @@ pub struct Border {
|
||||
pub hwnd: isize,
|
||||
pub id: String,
|
||||
pub monitor_idx: Option<usize>,
|
||||
pub render_target: Option<RenderTarget>,
|
||||
pub render_target: OnceLock<RenderTarget>,
|
||||
pub tracking_hwnd: isize,
|
||||
pub window_rect: Rect,
|
||||
pub window_kind: WindowKind,
|
||||
@@ -134,7 +136,7 @@ impl From<isize> for Border {
|
||||
hwnd: value,
|
||||
id: String::new(),
|
||||
monitor_idx: None,
|
||||
render_target: None,
|
||||
render_target: OnceLock::new(),
|
||||
tracking_hwnd: 0,
|
||||
window_rect: Rect::default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
@@ -182,7 +184,7 @@ impl Border {
|
||||
hwnd: 0,
|
||||
id: container_id,
|
||||
monitor_idx: Some(monitor_idx),
|
||||
render_target: None,
|
||||
render_target: OnceLock::new(),
|
||||
tracking_hwnd,
|
||||
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
@@ -241,14 +243,8 @@ 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!(self.hwnd)),
|
||||
hwnd: HWND(windows_api::as_ptr!(border.hwnd)),
|
||||
pixelSize: Default::default(),
|
||||
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
|
||||
};
|
||||
@@ -269,7 +265,7 @@ impl Border {
|
||||
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
|
||||
} {
|
||||
Ok(render_target) => unsafe {
|
||||
self.brush_properties = *BRUSH_PROPERTIES.deref();
|
||||
border.brush_properties = *BRUSH_PROPERTIES.deref();
|
||||
for window_kind in [
|
||||
WindowKind::Single,
|
||||
WindowKind::Stack,
|
||||
@@ -287,18 +283,24 @@ impl Border {
|
||||
};
|
||||
|
||||
if let Ok(brush) =
|
||||
render_target.CreateSolidColorBrush(&color, Some(&self.brush_properties))
|
||||
render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties))
|
||||
{
|
||||
self.brushes.insert(window_kind, brush);
|
||||
border.brushes.insert(window_kind, brush);
|
||||
}
|
||||
}
|
||||
|
||||
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||
|
||||
self.render_target = Some(RenderTarget(render_target));
|
||||
if border
|
||||
.render_target
|
||||
.set(RenderTarget(render_target.clone()))
|
||||
.is_err()
|
||||
{
|
||||
return Err(anyhow!("could not store border render target"));
|
||||
}
|
||||
|
||||
self.rounded_rect = {
|
||||
let radius = 8.0 + self.width as f32 / 2.0;
|
||||
border.rounded_rect = {
|
||||
let radius = 8.0 + border.width as f32 / 2.0;
|
||||
D2D1_ROUNDED_RECT {
|
||||
rect: Default::default(),
|
||||
radiusX: radius,
|
||||
@@ -306,7 +308,7 @@ impl Border {
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
Ok(border)
|
||||
},
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
@@ -392,63 +394,63 @@ impl Border {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if (!rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect))
|
||||
&& let Some(render_target) = (*border_pointer).render_target.as_ref()
|
||||
{
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
if !rect.is_same_size_as(&old_rect) {
|
||||
if let Some(render_target) = (*border_pointer).render_target.get() {
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +477,7 @@ impl Border {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||
if let Some(render_target) = (*border_pointer).render_target.get() {
|
||||
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
||||
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
||||
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod border;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::WindowKind;
|
||||
use crate::ring::Ring;
|
||||
use crate::windows_api;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
pub use border::Border;
|
||||
use crate::Colour;
|
||||
use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
pub use border::Border;
|
||||
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;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use strum::Display;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
@@ -74,10 +73,7 @@ impl Deref for RenderTarget {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Notification {
|
||||
Update(Option<isize>),
|
||||
ForceUpdate,
|
||||
}
|
||||
pub struct Notification(pub Option<isize>);
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct BorderInfo {
|
||||
@@ -106,21 +102,16 @@ fn event_rx() -> Receiver<Notification> {
|
||||
}
|
||||
|
||||
pub fn window_border(hwnd: isize) -> Option<BorderInfo> {
|
||||
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,
|
||||
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,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_notification(hwnd: Option<isize>) {
|
||||
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() {
|
||||
if event_tx().try_send(Notification(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
@@ -136,8 +127,6 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
let _ = destroy_border(border);
|
||||
}
|
||||
|
||||
drop(borders);
|
||||
|
||||
WINDOWS_BORDERS.lock().clear();
|
||||
|
||||
let mut remaining_hwnds = vec![];
|
||||
@@ -170,15 +159,13 @@ fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -188,7 +175,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
event_tx().send(Notification::Update(None))?;
|
||||
event_tx().send(Notification(None))?;
|
||||
|
||||
let mut previous_snapshot = Ring::default();
|
||||
let mut previous_pending_move_op = None;
|
||||
@@ -211,12 +198,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
let workspace_layer = state.monitors.elements()[focused_monitor_idx].workspaces()
|
||||
let workspace_layer = *state.monitors.elements()[focused_monitor_idx].workspaces()
|
||||
[focused_workspace_idx]
|
||||
.layer;
|
||||
.layer();
|
||||
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
let forced_update = matches!(notification, Notification::ForceUpdate);
|
||||
|
||||
drop(state);
|
||||
|
||||
@@ -226,7 +211,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = &ws.monocle_container {
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let window_kind = if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
@@ -239,17 +224,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -257,7 +231,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let window_kind = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
if c.locked {
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
@@ -287,74 +261,67 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
}
|
||||
BorderImplementation::Komorebi => {
|
||||
let should_process_notification = match notification {
|
||||
Notification::Update(notification_hwnd) => {
|
||||
let mut should_process_notification = true;
|
||||
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 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 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;
|
||||
}
|
||||
// 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 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_hwnd.unwrap_or_default())
|
||||
.is_some_and(|b| b.window_kind == WindowKind::Unfocused)
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
// 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
|
||||
&& let Some(Notification::Update(ref previous)) = previous_notification
|
||||
&& previous.unwrap_or_default() != notification_hwnd.unwrap_or_default()
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
should_process_notification
|
||||
}
|
||||
Notification::ForceUpdate => true,
|
||||
};
|
||||
if !should_process_notification && switch_focus_to_from_floating_window {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::debug!("monitor state matches latest snapshot, skipping notification");
|
||||
if let Some(ref previous) = previous_notification {
|
||||
if previous.0.unwrap_or_default() != notification.0.unwrap_or_default() {
|
||||
should_process_notification = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
@@ -381,7 +348,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Workspaces with tiling disabled don't have borders
|
||||
if !ws.tile {
|
||||
if !ws.tile() {
|
||||
// Remove all borders on this monitor
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
@@ -394,16 +361,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = &ws.monocle_container {
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let mut new_border = false;
|
||||
let focused_window_hwnd =
|
||||
monocle.focused_window().map(|w| w.hwnd).unwrap_or_default();
|
||||
let id = monocle.id.clone();
|
||||
let id = monocle.id().clone();
|
||||
let border = match borders.entry(id.clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(
|
||||
&monocle.id,
|
||||
monocle.id(),
|
||||
focused_window_hwnd,
|
||||
monitor_idx,
|
||||
) {
|
||||
@@ -448,11 +415,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
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();
|
||||
@@ -460,48 +422,22 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
windows_borders.insert(focused_window_hwnd, id);
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
// Remove all borders on this monitor except monocle
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| border_hwnd != b.hwnd,
|
||||
)?;
|
||||
|
||||
if ws.layer == WorkspaceLayer::Floating {
|
||||
handle_floating_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
ws,
|
||||
monitor_idx,
|
||||
foreground_window,
|
||||
layer_changed,
|
||||
forced_update,
|
||||
)?;
|
||||
|
||||
// Remove all borders on this monitor except monocle and floating borders
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| {
|
||||
border_hwnd != b.hwnd
|
||||
&& !ws
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.any(|w| w.hwnd == b.tracking_hwnd)
|
||||
},
|
||||
)?;
|
||||
} else {
|
||||
// Remove all borders on this monitor except monocle
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| border_hwnd != b.hwnd,
|
||||
)?;
|
||||
}
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let foreground_monitor_id =
|
||||
WindowsApi::monitor_from_window(foreground_hwnd);
|
||||
let is_maximized =
|
||||
foreground_monitor_id == m.id && WindowsApi::is_zoomed(foreground_hwnd);
|
||||
let is_maximized = foreground_monitor_id == m.id()
|
||||
&& WindowsApi::is_zoomed(foreground_hwnd);
|
||||
|
||||
if is_maximized {
|
||||
// Remove all borders on this monitor
|
||||
@@ -519,7 +455,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let mut container_and_floating_window_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
.map(|c| c.id.clone())
|
||||
.map(|c| c.id().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for w in ws.floating_windows() {
|
||||
@@ -537,7 +473,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
'containers: for (idx, c) in ws.containers().iter().enumerate() {
|
||||
let focused_window_hwnd =
|
||||
c.focused_window().map(|w| w.hwnd).unwrap_or_default();
|
||||
let id = c.id.clone();
|
||||
let id = c.id().clone();
|
||||
|
||||
// Get the border entry for this container from the map or create one
|
||||
let mut new_border = false;
|
||||
@@ -545,7 +481,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) =
|
||||
Border::create(&c.id, focused_window_hwnd, monitor_idx)
|
||||
Border::create(c.id(), focused_window_hwnd, monitor_idx)
|
||||
{
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
@@ -561,7 +497,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
|| focused_window_hwnd != foreground_window
|
||||
{
|
||||
if c.locked {
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
@@ -601,24 +537,19 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let rect = match WindowsApi::window_rect(focused_window_hwnd) {
|
||||
Ok(rect) => rect,
|
||||
Err(_) => {
|
||||
remove_border(&c.id, &mut borders, &mut windows_borders)?;
|
||||
remove_border(c.id(), &mut borders, &mut windows_borders)?;
|
||||
continue 'containers;
|
||||
}
|
||||
};
|
||||
border.window_rect = rect;
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
|
||||
let should_invalidate = new_border
|
||||
|| (last_focus_state != new_focus_state)
|
||||
|| layer_changed
|
||||
|| forced_update;
|
||||
|| layer_changed;
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -626,15 +557,56 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
windows_borders.insert(focused_window_hwnd, id);
|
||||
}
|
||||
|
||||
handle_floating_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
ws,
|
||||
monitor_idx,
|
||||
foreground_window,
|
||||
layer_changed,
|
||||
forced_update,
|
||||
)?;
|
||||
{
|
||||
for window in ws.floating_windows() {
|
||||
let mut new_border = false;
|
||||
let id = window.hwnd.to_string();
|
||||
let border = match borders.entry(id.clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(
|
||||
&window.hwnd.to_string(),
|
||||
window.hwnd,
|
||||
monitor_idx,
|
||||
) {
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let last_focus_state = border.window_kind;
|
||||
|
||||
let new_focus_state = if foreground_window == window.hwnd {
|
||||
WindowKind::Floating
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
};
|
||||
|
||||
border.window_kind = new_focus_state;
|
||||
|
||||
// Update the border's monitor idx in case it changed
|
||||
border.monitor_idx = Some(monitor_idx);
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
border.window_rect = rect;
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
|
||||
let 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -650,68 +622,6 @@ 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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use hex_color::HexColor;
|
||||
use komorebi_themes::Color32;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::SchemaGenerator;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::InstanceType;
|
||||
#[cfg(feature = "schemars")]
|
||||
@@ -8,7 +9,6 @@ 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(pub HexColor);
|
||||
pub struct Hex(HexColor);
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for Hex {
|
||||
@@ -6,17 +6,17 @@
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::ops::Deref;
|
||||
use windows::core::IUnknown;
|
||||
use windows::core::IUnknown_Vtbl;
|
||||
use windows::core::GUID;
|
||||
use windows::core::HRESULT;
|
||||
use windows::core::HSTRING;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::core::PWSTR;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::RECT;
|
||||
use windows::Win32::Foundation::SIZE;
|
||||
use windows::Win32::UI::Shell::Common::IObjectArray;
|
||||
use windows::core::GUID;
|
||||
use windows::core::HRESULT;
|
||||
use windows::core::HSTRING;
|
||||
use windows::core::IUnknown;
|
||||
use windows::core::IUnknown_Vtbl;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::core::PWSTR;
|
||||
use windows_core::BOOL;
|
||||
|
||||
type DesktopID = GUID;
|
||||
@@ -129,7 +129,7 @@ pub unsafe trait IApplicationView: IUnknown {
|
||||
pub unsafe fn get_app_user_model_id(&self, id: *mut PWSTR) -> HRESULT; // Proc17
|
||||
pub unsafe fn set_app_user_model_id(&self, id: PCWSTR) -> HRESULT;
|
||||
pub unsafe fn is_equal_by_app_user_model_id(&self, id: PCWSTR, out_result: *mut INT)
|
||||
-> HRESULT;
|
||||
-> HRESULT;
|
||||
|
||||
/*** IApplicationView methods ***/
|
||||
pub unsafe fn get_view_state(&self, out_state: *mut UINT) -> HRESULT; // Proc20
|
||||
|
||||
@@ -11,11 +11,11 @@ use interfaces::IServiceProvider;
|
||||
use std::ffi::c_void;
|
||||
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||
use windows::Win32::System::Com::COINIT_MULTITHREADED;
|
||||
use windows::Win32::System::Com::CoCreateInstance;
|
||||
use windows::Win32::System::Com::CoInitializeEx;
|
||||
use windows::Win32::System::Com::CoUninitialize;
|
||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||
use windows::Win32::System::Com::COINIT_MULTITHREADED;
|
||||
use windows_core::Interface;
|
||||
|
||||
struct ComInit();
|
||||
@@ -64,7 +64,7 @@ fn get_iapplication_view_collection(provider: &IServiceProvider) -> IApplication
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
|
||||
COM_INIT.with(|_| {
|
||||
let provider = get_iservice_provider();
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use getset::Getters;
|
||||
use nanoid::nanoid;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Lockable;
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Container {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub locked: bool,
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
windows: Ring<Window>,
|
||||
}
|
||||
|
||||
@@ -23,33 +22,22 @@ impl Default for Container {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: nanoid!(),
|
||||
locked: false,
|
||||
windows: Ring::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Lockable for Container {
|
||||
fn locked(&self) -> bool {
|
||||
self.locked
|
||||
}
|
||||
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self {
|
||||
self.locked = locked;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn hide(&self, omit: Option<isize>) {
|
||||
for window in self.windows().iter().rev() {
|
||||
let mut should_hide = omit.is_none();
|
||||
|
||||
if !should_hide
|
||||
&& let Some(omit) = omit
|
||||
&& omit != window.hwnd
|
||||
{
|
||||
should_hide = true
|
||||
if !should_hide {
|
||||
if let Some(omit) = omit {
|
||||
if omit != window.hwnd {
|
||||
should_hide = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_hide {
|
||||
@@ -81,10 +69,10 @@ impl Container {
|
||||
|
||||
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
|
||||
for window in self.windows() {
|
||||
if let Ok(window_exe) = window.exe()
|
||||
&& exe == window_exe
|
||||
{
|
||||
return Option::from(window.hwnd);
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,10 +81,10 @@ impl Container {
|
||||
|
||||
pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
|
||||
for (idx, window) in self.windows().iter().enumerate() {
|
||||
if let Ok(window_exe) = window.exe()
|
||||
&& exe == window_exe
|
||||
{
|
||||
return Option::from(idx);
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +144,6 @@ impl Container {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json;
|
||||
|
||||
#[test]
|
||||
fn test_contains_window() {
|
||||
@@ -263,40 +250,4 @@ mod tests {
|
||||
// Should return None since window 4 doesn't exist
|
||||
assert_eq!(container.idx_for_window(4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_with_missing_locked_field_defaults_to_false() {
|
||||
let json = r#"{
|
||||
"id": "test-1",
|
||||
"windows": { "elements": [], "focused": 0 }
|
||||
}"#;
|
||||
let container: Container = serde_json::from_str(json).expect("Should deserialize");
|
||||
|
||||
assert!(!container.locked);
|
||||
assert_eq!(container.id, "test-1");
|
||||
assert!(container.windows().is_empty());
|
||||
|
||||
let json = r#"{
|
||||
"id": "test-2",
|
||||
"windows": { "elements": [ { "hwnd": 5 }, { "hwnd": 9 } ], "focused": 1 }
|
||||
}"#;
|
||||
let container: Container = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(container.id, "test-2");
|
||||
assert!(!container.locked);
|
||||
assert_eq!(container.windows(), &[Window::from(5), Window::from(9)]);
|
||||
assert_eq!(container.focused_window_idx(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_and_deserializes() {
|
||||
let mut container = Container::default();
|
||||
container.set_locked(true);
|
||||
|
||||
let serialized = serde_json::to_string(&container).expect("Should serialize");
|
||||
let deserialized: Container =
|
||||
serde_json::from_str(&serialized).expect("Should deserialize");
|
||||
|
||||
assert!(deserialized.locked);
|
||||
assert_eq!(deserialized.id, container.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ use clap::ValueEnum;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::ser::SerializeSeq;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum AnimationStyle {
|
||||
Linear,
|
||||
@@ -39,81 +38,4 @@ pub enum AnimationStyle {
|
||||
EaseInBounce,
|
||||
EaseOutBounce,
|
||||
EaseInOutBounce,
|
||||
#[value(skip)]
|
||||
CubicBezier(f64, f64, f64, f64),
|
||||
}
|
||||
|
||||
// Custom serde implementation
|
||||
impl<'de> Deserialize<'de> for AnimationStyle {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct AnimationStyleVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for AnimationStyleVisitor {
|
||||
type Value = AnimationStyle;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string or an array of four f64 values")
|
||||
}
|
||||
|
||||
// Handle string variants (e.g., "EaseInOutExpo")
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
value.parse().map_err(|_| E::unknown_variant(value, &[]))
|
||||
}
|
||||
|
||||
// Handle CubicBezier array (e.g., [0.32, 0.72, 0.0, 1.0])
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let x1 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
|
||||
let y1 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
|
||||
let x2 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
|
||||
let y2 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
|
||||
|
||||
// Ensure no extra elements
|
||||
if seq.next_element::<serde::de::IgnoredAny>()?.is_some() {
|
||||
return Err(serde::de::Error::invalid_length(5, &self));
|
||||
}
|
||||
|
||||
Ok(AnimationStyle::CubicBezier(x1, y1, x2, y2))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(AnimationStyleVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AnimationStyle {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
// Serialize CubicBezier as an array
|
||||
AnimationStyle::CubicBezier(x1, y1, x2, y2) => {
|
||||
let mut seq = serializer.serialize_seq(Some(4))?;
|
||||
seq.serialize_element(x1)?;
|
||||
seq.serialize_element(y1)?;
|
||||
seq.serialize_element(x2)?;
|
||||
seq.serialize_element(y2)?;
|
||||
seq.end()
|
||||
}
|
||||
// Serialize all other variants as strings
|
||||
_ => serializer.serialize_str(&self.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,14 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Rect;
|
||||
use super::custom_layout::Column;
|
||||
use super::custom_layout::ColumnSplit;
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Rect;
|
||||
|
||||
pub trait Arrangement {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
@@ -23,9 +21,6 @@ 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>;
|
||||
}
|
||||
|
||||
@@ -38,99 +33,9 @@ impl Arrangement for DefaultLayout {
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
focused_idx: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
latest_layout: &[Rect],
|
||||
) -> Vec<Rect> {
|
||||
let len = usize::from(len);
|
||||
let mut dimensions = match self {
|
||||
Self::Scrolling => {
|
||||
let column_count = layout_options
|
||||
.and_then(|o| o.scrolling.map(|s| s.columns))
|
||||
.unwrap_or(3);
|
||||
|
||||
let column_width = area.right / column_count as i32;
|
||||
let mut layouts = Vec::with_capacity(len);
|
||||
|
||||
let visible_columns = area.right / column_width;
|
||||
let keep_centered = layout_options
|
||||
.and_then(|o| {
|
||||
o.scrolling
|
||||
.map(|s| s.center_focused_column.unwrap_or_default())
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let first_visible: isize = if focused_idx == 0 {
|
||||
// if focused idx is 0, we are at the beginning of the scrolling strip
|
||||
0
|
||||
} else {
|
||||
let previous_first_visible = if latest_layout.is_empty() {
|
||||
0
|
||||
} else {
|
||||
// previous first_visible based on the left position of the first visible window
|
||||
let left_edge = area.left;
|
||||
latest_layout
|
||||
.iter()
|
||||
.position(|rect| rect.left >= left_edge)
|
||||
.unwrap_or(0) as isize
|
||||
};
|
||||
|
||||
let focused_idx = focused_idx as isize;
|
||||
|
||||
// if center_focused_column is enabled, and we have an odd number of visible columns,
|
||||
// center the focused window column
|
||||
if keep_centered && visible_columns % 2 == 1 {
|
||||
let center_offset = visible_columns as isize / 2;
|
||||
(focused_idx - center_offset).max(0).min(
|
||||
(len as isize)
|
||||
.saturating_sub(visible_columns as isize)
|
||||
.max(0),
|
||||
)
|
||||
} else {
|
||||
if focused_idx < previous_first_visible {
|
||||
// focused window is off the left edge, we need to scroll left
|
||||
focused_idx
|
||||
} else if focused_idx >= previous_first_visible + visible_columns as isize {
|
||||
// focused window is off the right edge, we need to scroll right
|
||||
// and make sure it's the last visible window
|
||||
(focused_idx + 1 - visible_columns as isize).max(0)
|
||||
} else {
|
||||
// focused window is already visible, we don't need to scroll
|
||||
previous_first_visible
|
||||
}
|
||||
.min(
|
||||
(len as isize)
|
||||
.saturating_sub(visible_columns as isize)
|
||||
.max(0),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
for i in 0..len {
|
||||
let position = (i as isize) - first_visible;
|
||||
let left = area.left + (position as i32 * column_width);
|
||||
|
||||
layouts.push(Rect {
|
||||
left,
|
||||
top: area.top,
|
||||
right: column_width,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::BSP => recursive_fibonacci(
|
||||
0,
|
||||
len,
|
||||
@@ -155,9 +60,10 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) && let 2.. = len
|
||||
{
|
||||
columns_reverse(&mut layouts);
|
||||
) {
|
||||
if let 2.. = len {
|
||||
columns_reverse(&mut layouts);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -179,9 +85,10 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) && let 2.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts);
|
||||
) {
|
||||
if let 2.. = len {
|
||||
rows_reverse(&mut layouts);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -232,23 +139,25 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) && let 2.. = len
|
||||
{
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left;
|
||||
}
|
||||
primary.left = rest[0].left + rest[0].right;
|
||||
}
|
||||
primary.left = rest[0].left + rest[0].right;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) && let 3.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
) {
|
||||
if let 3.. = len {
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -302,23 +211,25 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) && let 2.. = len
|
||||
{
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
primary.left = rest[0].left;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left + primary.right;
|
||||
primary.left = rest[0].left;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left + primary.right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) && let 3.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
) {
|
||||
if let 3.. = len {
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -369,23 +280,25 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) && let 2.. = len
|
||||
{
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
for rect in rest.iter_mut() {
|
||||
rect.top = primary.top;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.top = primary.top;
|
||||
}
|
||||
primary.top = rest[0].top + rest[0].bottom;
|
||||
}
|
||||
primary.top = rest[0].top + rest[0].bottom;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) && let 3.. = len
|
||||
{
|
||||
columns_reverse(&mut layouts[1..]);
|
||||
) {
|
||||
if let 3.. = len {
|
||||
columns_reverse(&mut layouts[1..]);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -494,9 +407,10 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) && let 4.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts[2..]);
|
||||
) {
|
||||
if let 4.. = len {
|
||||
rows_reverse(&mut layouts[2..]);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -573,9 +487,6 @@ 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();
|
||||
@@ -630,7 +541,7 @@ impl Arrangement for CustomLayout {
|
||||
};
|
||||
|
||||
match column {
|
||||
Column::Primary(Some(_)) => {
|
||||
Column::Primary(Option::Some(_)) => {
|
||||
let main_column_area = if idx == 0 {
|
||||
Self::main_column_area(area, primary_right, None)
|
||||
} else {
|
||||
@@ -762,67 +673,67 @@ fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Optio
|
||||
|
||||
// This needs to be aware of layout flips
|
||||
for (i, opt) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(resize_ref) = opt
|
||||
&& i > 0
|
||||
{
|
||||
if resize_ref.left != 0 {
|
||||
#[allow(clippy::if_not_else)]
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 != 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
if let Some(resize_ref) = opt {
|
||||
if i > 0 {
|
||||
if resize_ref.left != 0 {
|
||||
#[allow(clippy::if_not_else)]
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 != 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 == 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.right += resize_ref.left;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: resize_ref.left,
|
||||
bottom: 0,
|
||||
});
|
||||
for n in range {
|
||||
let should_adjust = n % 2 == 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.right += resize_ref.left;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: resize_ref.left,
|
||||
bottom: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||
rr.left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||
rr.left = 0;
|
||||
}
|
||||
}
|
||||
if resize_ref.top != 0 {
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 == 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
if resize_ref.top != 0 {
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 == 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 != 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.bottom += resize_ref.top;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: resize_ref.top,
|
||||
});
|
||||
for n in range {
|
||||
let should_adjust = n % 2 != 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.bottom += resize_ref.top;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: resize_ref.top,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||
resize.top = 0;
|
||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||
resize.top = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -907,7 +818,7 @@ fn recursive_fibonacci(
|
||||
right: resized.right,
|
||||
bottom: resized.bottom,
|
||||
}]
|
||||
} else if !idx.is_multiple_of(2) {
|
||||
} else if idx % 2 != 0 {
|
||||
let mut res = vec![Rect {
|
||||
left: resized.left,
|
||||
top: main_y,
|
||||
@@ -1204,37 +1115,6 @@ fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rec
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_scrolling_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
|
||||
if len <= 1 {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (i, rect) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
let is_leftmost = i == 0;
|
||||
let is_rightmost = i == len - 1;
|
||||
|
||||
resize_left(&mut result[i], rect.left);
|
||||
resize_right(&mut result[i], rect.right);
|
||||
resize_top(&mut result[i], rect.top);
|
||||
resize_bottom(&mut result[i], rect.bottom);
|
||||
|
||||
if !is_leftmost && rect.left != 0 {
|
||||
resize_right(&mut result[i - 1], rect.left);
|
||||
}
|
||||
|
||||
if !is_rightmost && rect.right != 0 {
|
||||
resize_left(&mut result[i + 1], rect.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn resize_left(rect: &mut Rect, resize: i32) {
|
||||
rect.left += resize / 2;
|
||||
rect.right += -resize / 2;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config_generation::ApplicationConfiguration;
|
||||
use crate::config_generation::ApplicationOptions;
|
||||
use crate::config_generation::MatchingRule;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -36,12 +36,12 @@ impl DerefMut for ApplicationSpecificConfiguration {
|
||||
}
|
||||
|
||||
impl ApplicationSpecificConfiguration {
|
||||
pub fn load(pathbuf: &PathBuf) -> eyre::Result<Self> {
|
||||
pub fn load(pathbuf: &PathBuf) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(pathbuf)?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
}
|
||||
|
||||
pub fn format(pathbuf: &PathBuf) -> eyre::Result<String> {
|
||||
pub fn format(pathbuf: &PathBuf) -> Result<String> {
|
||||
Ok(serde_json::to_string_pretty(&Self::load(pathbuf)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
@@ -142,11 +142,11 @@ impl ApplicationConfiguration {
|
||||
pub struct ApplicationConfigurationGenerator;
|
||||
|
||||
impl ApplicationConfigurationGenerator {
|
||||
pub fn load(content: &str) -> eyre::Result<Vec<ApplicationConfiguration>> {
|
||||
pub fn load(content: &str) -> Result<Vec<ApplicationConfiguration>> {
|
||||
Ok(serde_yaml::from_str(content)?)
|
||||
}
|
||||
|
||||
pub fn format(content: &str) -> eyre::Result<String> {
|
||||
pub fn format(content: &str) -> Result<String> {
|
||||
let mut cfgen = Self::load(content)?;
|
||||
for cfg in &mut cfgen {
|
||||
cfg.populate_default_matching_strategies();
|
||||
@@ -156,10 +156,7 @@ impl ApplicationConfigurationGenerator {
|
||||
Ok(serde_yaml::to_string(&cfgen)?)
|
||||
}
|
||||
|
||||
fn merge(
|
||||
base_content: &str,
|
||||
override_content: &str,
|
||||
) -> eyre::Result<Vec<ApplicationConfiguration>> {
|
||||
fn merge(base_content: &str, override_content: &str) -> Result<Vec<ApplicationConfiguration>> {
|
||||
let base_cfgen = Self::load(base_content)?;
|
||||
let override_cfgen = Self::load(override_content)?;
|
||||
|
||||
@@ -185,7 +182,7 @@ impl ApplicationConfigurationGenerator {
|
||||
pub fn generate_pwsh(
|
||||
base_content: &str,
|
||||
override_content: Option<&str>,
|
||||
) -> eyre::Result<Vec<String>> {
|
||||
) -> Result<Vec<String>> {
|
||||
let mut cfgen = if let Some(override_content) = override_content {
|
||||
Self::merge(base_content, override_content)?
|
||||
} else {
|
||||
@@ -236,10 +233,7 @@ impl ApplicationConfigurationGenerator {
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
pub fn generate_ahk(
|
||||
base_content: &str,
|
||||
override_content: Option<&str>,
|
||||
) -> eyre::Result<Vec<String>> {
|
||||
pub fn generate_ahk(base_content: &str, override_content: Option<&str>) -> Result<Vec<String>> {
|
||||
let mut cfgen = if let Some(override_content) = override_content {
|
||||
Self::merge(base_content, override_content)?
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::bail;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
@@ -9,6 +5,12 @@ use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::Rect;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -30,7 +32,7 @@ impl DerefMut for CustomLayout {
|
||||
}
|
||||
|
||||
impl CustomLayout {
|
||||
pub fn from_path<P: AsRef<Path>>(path: P) -> eyre::Result<Self> {
|
||||
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let layout: Self = match path.extension() {
|
||||
Some(extension) if extension == "yaml" || extension == "yml" => {
|
||||
@@ -39,7 +41,7 @@ impl CustomLayout {
|
||||
Some(extension) if extension == "json" => {
|
||||
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
||||
}
|
||||
_ => bail!("custom layouts must be json or yaml files"),
|
||||
_ => return Err(anyhow!("custom layouts must be json or yaml files")),
|
||||
};
|
||||
|
||||
if !layout.is_valid() {
|
||||
|
||||
@@ -21,26 +21,9 @@ 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,
|
||||
/// With an odd number of visible columns, keep the focused window column centered
|
||||
pub center_focused_column: Option<bool>,
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
@@ -48,7 +31,6 @@ impl DefaultLayout {
|
||||
n if n > 1 => 1,
|
||||
_ => 0,
|
||||
},
|
||||
Self::Scrolling => 0,
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
@@ -71,7 +53,6 @@ impl DefaultLayout {
|
||||
_ => len.saturating_sub(1),
|
||||
},
|
||||
DefaultLayout::RightMainVerticalStack => 0,
|
||||
DefaultLayout::Scrolling => len.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +75,6 @@ impl DefaultLayout {
|
||||
| Self::RightMainVerticalStack
|
||||
| Self::HorizontalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::Scrolling
|
||||
) {
|
||||
return None;
|
||||
};
|
||||
@@ -189,15 +169,13 @@ impl DefaultLayout {
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Scrolling,
|
||||
Self::Scrolling => Self::BSP,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn cycle_previous(self) -> Self {
|
||||
match self {
|
||||
Self::Scrolling => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use super::DefaultLayout;
|
||||
use super::OperationDirection;
|
||||
use super::custom_layout::Column;
|
||||
use super::custom_layout::ColumnSplit;
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use super::custom_layout::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::OperationDirection;
|
||||
|
||||
pub trait Direction {
|
||||
fn index_in_direction(
|
||||
@@ -102,17 +102,15 @@ 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.is_multiple_of(2),
|
||||
Self::BSP => idx != count - 1 && idx % 2 != 0,
|
||||
Self::Columns => false,
|
||||
Self::Rows => idx != count - 1,
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
|
||||
Self::HorizontalStack => idx == 0,
|
||||
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
Self::BSP => idx != 0,
|
||||
@@ -122,10 +120,9 @@ 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.is_multiple_of(2) && idx != count - 1,
|
||||
Self::BSP => idx % 2 == 0 && idx != count - 1,
|
||||
Self::Columns => idx != count - 1,
|
||||
Self::Rows => false,
|
||||
Self::VerticalStack => idx == 0,
|
||||
@@ -136,7 +133,6 @@ impl Direction for DefaultLayout {
|
||||
_ => idx < 2,
|
||||
},
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => idx != count - 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -149,7 +145,7 @@ impl Direction for DefaultLayout {
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
if idx.is_multiple_of(2) {
|
||||
if idx % 2 == 0 {
|
||||
idx - 1
|
||||
} else {
|
||||
idx - 2
|
||||
@@ -162,7 +158,6 @@ impl Direction for DefaultLayout {
|
||||
| Self::RightMainVerticalStack => idx - 1,
|
||||
Self::HorizontalStack => 0,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +176,6 @@ impl Direction for DefaultLayout {
|
||||
Self::Columns => unreachable!(),
|
||||
Self::HorizontalStack => 1,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +187,7 @@ impl Direction for DefaultLayout {
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
if idx.is_multiple_of(2) {
|
||||
if idx % 2 == 0 {
|
||||
idx - 2
|
||||
} else {
|
||||
idx - 1
|
||||
@@ -209,7 +203,6 @@ impl Direction for DefaultLayout {
|
||||
_ => 0,
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => idx - 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +223,6 @@ impl Direction for DefaultLayout {
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => idx + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::KomorebiTheme;
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
use crate::KomorebiTheme;
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
@@ -28,9 +29,6 @@ pub use direction::Direction;
|
||||
pub use layout::Layout;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use pathext::PathExt;
|
||||
pub use pathext::ResolvedPathBuf;
|
||||
pub use pathext::replace_env_in_path;
|
||||
pub use pathext::resolve_option_hashmap_usize_path;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
@@ -46,8 +44,6 @@ 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")]
|
||||
@@ -109,8 +105,7 @@ pub enum SocketMessage {
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(DefaultLayout),
|
||||
CycleLayout(CycleDirection),
|
||||
ScrollingLayoutColumns(NonZeroUsize),
|
||||
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
ChangeLayoutCustom(PathBuf),
|
||||
FlipLayout(Axis),
|
||||
ToggleWorkspaceWindowContainerBehaviour,
|
||||
ToggleWorkspaceFloatOverride,
|
||||
@@ -128,8 +123,8 @@ pub enum SocketMessage {
|
||||
RetileWithResizeDimensions,
|
||||
QuickSave,
|
||||
QuickLoad,
|
||||
Save(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
Load(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
Save(PathBuf),
|
||||
Load(PathBuf),
|
||||
CycleFocusMonitor(CycleDirection),
|
||||
CycleFocusWorkspace(CycleDirection),
|
||||
CycleFocusEmptyWorkspace(CycleDirection),
|
||||
@@ -152,28 +147,23 @@ pub enum SocketMessage {
|
||||
WorkspaceName(usize, usize, String),
|
||||
WorkspaceLayout(usize, usize, DefaultLayout),
|
||||
NamedWorkspaceLayout(String, DefaultLayout),
|
||||
WorkspaceLayoutCustom(usize, usize, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
NamedWorkspaceLayoutCustom(String, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
WorkspaceLayoutCustom(usize, usize, PathBuf),
|
||||
NamedWorkspaceLayoutCustom(String, PathBuf),
|
||||
WorkspaceLayoutRule(usize, usize, usize, DefaultLayout),
|
||||
NamedWorkspaceLayoutRule(String, usize, DefaultLayout),
|
||||
WorkspaceLayoutCustomRule(
|
||||
usize,
|
||||
usize,
|
||||
usize,
|
||||
#[serde_as(as = "ResolvedPathBuf")] PathBuf,
|
||||
),
|
||||
NamedWorkspaceLayoutCustomRule(String, usize, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
WorkspaceLayoutCustomRule(usize, usize, usize, PathBuf),
|
||||
NamedWorkspaceLayoutCustomRule(String, usize, PathBuf),
|
||||
ClearWorkspaceLayoutRules(usize, usize),
|
||||
ClearNamedWorkspaceLayoutRules(String),
|
||||
ToggleWorkspaceLayer,
|
||||
// Configuration
|
||||
ReloadConfiguration,
|
||||
ReplaceConfiguration(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
ReloadStaticConfiguration(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
ReplaceConfiguration(PathBuf),
|
||||
ReloadStaticConfiguration(PathBuf),
|
||||
WatchConfiguration(bool),
|
||||
CompleteConfiguration,
|
||||
AltFocusHack(bool),
|
||||
Theme(Box<KomorebiTheme>),
|
||||
Theme(KomorebiTheme),
|
||||
Animation(bool, Option<AnimationPrefix>),
|
||||
AnimationDuration(u64, Option<AnimationPrefix>),
|
||||
AnimationFps(u64),
|
||||
@@ -202,7 +192,6 @@ pub enum SocketMessage {
|
||||
StackbarFontFamily(Option<String>),
|
||||
WorkAreaOffset(Rect),
|
||||
MonitorWorkAreaOffset(usize, Rect),
|
||||
WorkspaceWorkAreaOffset(usize, usize, Rect),
|
||||
ToggleWindowBasedWorkAreaOffset,
|
||||
ResizeDelta(i32),
|
||||
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
@@ -213,9 +202,6 @@ pub enum SocketMessage {
|
||||
ClearNamedWorkspaceRules(String),
|
||||
ClearAllWorkspaceRules,
|
||||
EnforceWorkspaceRules,
|
||||
SessionFloatRule,
|
||||
SessionFloatRules,
|
||||
ClearSessionFloatRules,
|
||||
#[serde(alias = "FloatRule")]
|
||||
IgnoreRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
@@ -248,7 +234,7 @@ pub enum SocketMessage {
|
||||
}
|
||||
|
||||
impl SocketMessage {
|
||||
pub fn as_bytes(&self) -> eyre::Result<Vec<u8>> {
|
||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
||||
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
@@ -256,7 +242,7 @@ impl SocketMessage {
|
||||
impl FromStr for SocketMessage {
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn from_str(s: &str) -> eyre::Result<Self, Self::Err> {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_str(s)
|
||||
}
|
||||
}
|
||||
@@ -343,9 +329,6 @@ pub enum StateQuery {
|
||||
FocusedContainerIndex,
|
||||
FocusedWindowIndex,
|
||||
FocusedWorkspaceName,
|
||||
FocusedWorkspaceLayout,
|
||||
FocusedContainerKind,
|
||||
Version,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -381,21 +364,6 @@ 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(
|
||||
@@ -415,59 +383,17 @@ pub enum WindowContainerBehaviour {
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum FloatingLayerBehaviour {
|
||||
/// Tile new windows (unless they match a float rule or float override is active)
|
||||
/// Tile new windows (unless they match a float rule)
|
||||
#[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,
|
||||
)]
|
||||
#[derive(Clone, Copy, Debug, 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,
|
||||
@@ -475,22 +401,19 @@ pub enum MoveBehaviour {
|
||||
NoOp,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[derive(Clone, Copy, Debug, 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 {
|
||||
/// END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
/// 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,
|
||||
@@ -498,13 +421,10 @@ pub enum HidingBehaviour {
|
||||
Cloak,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[derive(Clone, Copy, Debug, 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,
|
||||
@@ -533,40 +453,45 @@ impl Sizing {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum WindowHandlingBehaviour {
|
||||
#[default]
|
||||
Sync,
|
||||
Async,
|
||||
}
|
||||
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"))?;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
resolved_path.extend(home.components());
|
||||
resolved = true;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes() {
|
||||
// Set a variable for testing
|
||||
unsafe {
|
||||
std::env::set_var("VAR", "VALUE");
|
||||
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 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"));
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use super::Axis;
|
||||
use super::direction::Direction;
|
||||
use super::Axis;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
|
||||
@@ -1,195 +1,48 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::env;
|
||||
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;
|
||||
}
|
||||
|
||||
/// Blanket implementation for all types that can be converted to a `Path`.
|
||||
impl<P: AsRef<Path>> PathExt for P {
|
||||
impl PathExt for PathBuf {
|
||||
fn replace_env(&self) -> PathBuf {
|
||||
let mut out = PathBuf::new();
|
||||
let mut result = PathBuf::new();
|
||||
|
||||
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..])
|
||||
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 {
|
||||
// not a variable
|
||||
None
|
||||
}
|
||||
};
|
||||
// Extract the variable name after `$`
|
||||
stripped_segment
|
||||
};
|
||||
|
||||
// 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) = std::env::var_os(var) {
|
||||
out.push(value);
|
||||
continue;
|
||||
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
|
||||
}
|
||||
|
||||
// if not a variable, or a value couldn't be obtained from environemnt
|
||||
// then push the component as is
|
||||
out.push(c);
|
||||
}
|
||||
|
||||
// other components are pushed as is
|
||||
_ => out.push(c),
|
||||
_ => {
|
||||
// Add other components (e.g., root, parent) as-is
|
||||
result.push(component.as_os_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(generator: &mut schemars::SchemaGenerator) -> schemars::schema::Schema {
|
||||
<PathBuf as schemars::JsonSchema>::json_schema(generator)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
unsafe {
|
||||
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
|
||||
unsafe {
|
||||
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"));
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,6 @@ impl Rect {
|
||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||
self.right == rhs.right && self.bottom == rhs.bottom
|
||||
}
|
||||
|
||||
pub fn has_same_position_as(&self, rhs: &Self) -> bool {
|
||||
self.left == rhs.left && self.top == rhs.top
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
|
||||
@@ -44,15 +44,13 @@ pub fn send_notification(hwnd: isize) {
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,10 +5,11 @@ 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 lockable_sequence;
|
||||
pub mod locked_deque;
|
||||
pub mod monitor;
|
||||
pub mod monitor_reconciliator;
|
||||
pub mod process_command;
|
||||
@@ -39,15 +40,15 @@ use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use colour::*;
|
||||
pub use core::*;
|
||||
pub use komorebi_themes::colour::*;
|
||||
pub use process_command::*;
|
||||
pub use process_event::*;
|
||||
pub use static_config::*;
|
||||
@@ -62,8 +63,7 @@ use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use crate::core::config_generation::WorkspaceMatchingRule;
|
||||
use color_eyre::eyre;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use color_eyre::Result;
|
||||
use os_info::Version;
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::RwLock;
|
||||
@@ -72,8 +72,8 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixStream;
|
||||
use which::which;
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
use winreg::RegKey;
|
||||
|
||||
lazy_static! {
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
@@ -159,15 +159,7 @@ lazy_static! {
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
]));
|
||||
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 FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
@@ -196,25 +188,26 @@ 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 = home_path.replace_env();
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
|
||||
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",
|
||||
);
|
||||
}
|
||||
})
|
||||
};
|
||||
pub static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
|
||||
pub static ref AHK_EXE: String = {
|
||||
let mut ahk: String = String::from("autohotkey.exe");
|
||||
|
||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE")
|
||||
&& which(&komorebi_ahk_exe).is_ok() {
|
||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
|
||||
if which(&komorebi_ahk_exe).is_ok() {
|
||||
ahk = komorebi_ahk_exe;
|
||||
}
|
||||
}
|
||||
|
||||
ahk
|
||||
};
|
||||
@@ -233,8 +226,6 @@ 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);
|
||||
@@ -248,19 +239,6 @@ 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);
|
||||
|
||||
/// A trait for types that can be marked as locked or unlocked.
|
||||
pub trait Lockable {
|
||||
/// Returns `true` if the item is locked.
|
||||
fn locked(&self) -> bool;
|
||||
/// Sets the locked state of the item.
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
@@ -305,34 +283,23 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
current
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
Monitor(MonitorNotification),
|
||||
VirtualDesktop(VirtualDesktopNotification),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum VirtualDesktopNotification {
|
||||
EnteredAssociatedVirtualDesktop,
|
||||
LeftAssociatedVirtualDesktop,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(
|
||||
notification: Notification,
|
||||
state_has_been_modified: bool,
|
||||
) -> eyre::Result<()> {
|
||||
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
|
||||
let is_override_event = matches!(
|
||||
notification.event,
|
||||
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
|
||||
@@ -413,7 +380,7 @@ pub fn notify_subscribers(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_configuration() -> eyre::Result<()> {
|
||||
pub fn load_configuration() -> Result<()> {
|
||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::Lockable;
|
||||
|
||||
/// A sequence supporting insertion, removal, and swapping of elements while preserving the absolute
|
||||
/// positions of locked items.
|
||||
pub trait LockableSequence<T: Lockable> {
|
||||
/// Inserts a value at `idx`, keeping locked elements at their absolute positions.
|
||||
fn insert_respecting_locks(&mut self, idx: usize, value: T) -> usize;
|
||||
/// Removes the element at `idx`, keeping locked elements at their absolute positions.
|
||||
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T>;
|
||||
/// Swaps the elements at indices `i` and `j`, keeping locked elements at their absolute positions.
|
||||
fn swap_respecting_locks(&mut self, i: usize, j: usize);
|
||||
}
|
||||
|
||||
impl<T: Lockable> LockableSequence<T> for VecDeque<T> {
|
||||
/// Insert `value` at logical index `idx`, with trying to keep locked elements
|
||||
/// (`is_locked()`) anchored at their original positions.
|
||||
///
|
||||
/// Returns the final index of the inserted element.
|
||||
fn insert_respecting_locks(&mut self, mut idx: usize, value: T) -> usize {
|
||||
// 1. Bounds check: if index is out of range, simply append.
|
||||
if idx >= self.len() {
|
||||
self.push_back(value);
|
||||
return self.len() - 1; // last index
|
||||
}
|
||||
|
||||
// 2. Normal VecDeque insertion
|
||||
self.insert(idx, value);
|
||||
|
||||
// 3. Walk left-to-right once, swapping any misplaced locked element. After
|
||||
// the VecDeque::insert all items after `idx` have moved right by one. For every locked
|
||||
// element that is now to the right of an unlocked one, swap it back left exactly once.
|
||||
for index in (idx + 1)..self.len() {
|
||||
if self[index].locked() && !self[index - 1].locked() {
|
||||
self.swap(index - 1, index);
|
||||
|
||||
// If the element we just inserted participated in the swap,
|
||||
// update `idx` so we can return its final location.
|
||||
if idx == index - 1 {
|
||||
idx = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
/// Remove element at `idx`, with trying to keep locked elements
|
||||
/// (`is_locked()`) anchored at their original positions.
|
||||
///
|
||||
/// Returns the removed element, or `None` if `idx` is out of bounds.
|
||||
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T> {
|
||||
// 1. Bounds check: if index is out of range, do nothing.
|
||||
if idx >= self.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Remove the element at the requested index.
|
||||
// All elements after idx are now shifted left by 1.
|
||||
let removed = self.remove(idx)?;
|
||||
|
||||
// 3. If less than 2 elements remain, nothing to shift.
|
||||
if self.len() < 2 {
|
||||
return Some(removed);
|
||||
}
|
||||
|
||||
// 4. Iterate from the element just after the removed spot up to the second-to-last
|
||||
// element, right-to-left. This loop "fixes" locked elements that were shifted left
|
||||
// off their anchored positions: If a locked element now has an unlocked element
|
||||
// to its right, swap them back to restore locked order.
|
||||
for index in (idx..self.len() - 1).rev() {
|
||||
// If current is locked and the next one is not locked, swap them.
|
||||
if self[index].locked() && !self[index + 1].locked() {
|
||||
self.swap(index, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Return the removed value.
|
||||
Some(removed)
|
||||
}
|
||||
|
||||
/// Swaps the elements at indices `i` and `j`, along with their `locked` status, ensuring
|
||||
/// the lock state remains associated with the position rather than the element itself.
|
||||
fn swap_respecting_locks(&mut self, i: usize, j: usize) {
|
||||
self.swap(i, j);
|
||||
let locked_i = self[i].locked();
|
||||
let locked_j = self[j].locked();
|
||||
self[i].set_locked(locked_j);
|
||||
self[j].set_locked(locked_i);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct TestItem {
|
||||
val: i32,
|
||||
locked: bool,
|
||||
}
|
||||
|
||||
impl Lockable for TestItem {
|
||||
fn locked(&self) -> bool {
|
||||
self.locked
|
||||
}
|
||||
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self {
|
||||
self.locked = locked;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn vals(v: &VecDeque<TestItem>) -> Vec<i32> {
|
||||
v.iter().map(|x| x.val).collect()
|
||||
}
|
||||
|
||||
fn test_deque(items: &[(i32, bool)]) -> VecDeque<TestItem> {
|
||||
items
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|(val, locked)| TestItem { val, locked })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_respecting_locks() {
|
||||
// Test case 1: Basic insertion with locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
// Insert at index 0, should shift elements while keeping index 2 locked
|
||||
ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
|
||||
assert_eq!(vals(&ring), vec![99, 0, 2, 1, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 2: Insert at a locked index (should insert after locked)
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
// Try to insert at locked index 2, should insert at index 3 instead
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
2,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 3);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 99, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 3: Multiple locked indices
|
||||
{
|
||||
// Lock index 1 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
|
||||
// Insert at index 0, should maintain locked indices
|
||||
ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Elements '1' and '3' remain at indices 1 and 3
|
||||
assert_eq!(vals(&ring), vec![99, 1, 0, 3, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 4: Insert at end
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
5,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
|
||||
}
|
||||
|
||||
// Test case 5: Empty ring
|
||||
{
|
||||
let mut ring = test_deque(&[]);
|
||||
// Insert into empty deque
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 0);
|
||||
assert_eq!(vals(&ring), vec![99]);
|
||||
}
|
||||
|
||||
// Test case 6: All indices locked
|
||||
{
|
||||
// Lock all indices
|
||||
let mut ring = test_deque(&[(0, true), (1, true), (2, true), (3, true), (4, true)]);
|
||||
// Try to insert at index 2, should insert at the end
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
2,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
|
||||
}
|
||||
|
||||
// Test case 7: Consecutive locked indices
|
||||
{
|
||||
// Lock index 2 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, true), (4, false)]);
|
||||
// Insert at index 1, should maintain consecutive locked indices
|
||||
ring.insert_respecting_locks(
|
||||
1,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Elements '2' and '3' remain at indices 2 and 3
|
||||
assert_eq!(vals(&ring), vec![0, 99, 2, 3, 1, 4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_respecting_locks() {
|
||||
// Test case 1: Remove a non-locked index before a locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(0);
|
||||
assert_eq!(removed.map(|x| x.val), Some(0));
|
||||
// Elements '2' remain at index 2
|
||||
assert_eq!(vals(&ring), vec![1, 3, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 2: Remove a locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(2);
|
||||
assert_eq!(removed.map(|x| x.val), Some(2));
|
||||
// Elements should stay at the same places
|
||||
assert_eq!(vals(&ring), vec![0, 1, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 3: Remove an index after a locked index
|
||||
{
|
||||
// Lock index 1
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(3);
|
||||
assert_eq!(removed.map(|x| x.val), Some(3));
|
||||
// Elements should stay at the same places
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 4: Multiple locked indices
|
||||
{
|
||||
// Lock index 1 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(0);
|
||||
assert_eq!(removed.map(|x| x.val), Some(0));
|
||||
// Elements '1' and '3' remain at indices '1' and '3'
|
||||
assert_eq!(vals(&ring), vec![2, 1, 4, 3]);
|
||||
}
|
||||
|
||||
// Test case 5: Remove the last element
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(4);
|
||||
assert_eq!(removed.map(|x| x.val), Some(4));
|
||||
// Index 2 should still be at the same place
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
|
||||
}
|
||||
|
||||
// Test case 6: Invalid index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(10);
|
||||
assert_eq!(removed, None);
|
||||
// Deque unchanged
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 7: Remove enough elements to make a locked index invalid
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true)]);
|
||||
ring.remove_respecting_locks(0);
|
||||
// Index 2 should now be '1'
|
||||
assert_eq!(vals(&ring), vec![1, 2]);
|
||||
}
|
||||
|
||||
// Test case 8: Removing an element before multiple locked indices
|
||||
{
|
||||
// Lock index 2 and 4
|
||||
let mut ring = test_deque(&[
|
||||
(0, false),
|
||||
(1, false),
|
||||
(2, true),
|
||||
(3, false),
|
||||
(4, true),
|
||||
(5, false),
|
||||
]);
|
||||
let removed = ring.remove_respecting_locks(1);
|
||||
assert_eq!(removed.map(|x| x.val), Some(1));
|
||||
// Both indices should still be at the same place
|
||||
assert_eq!(vals(&ring), vec![0, 3, 2, 5, 4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swap_respecting_locks_various_cases() {
|
||||
// Swap unlocked and locked
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false)]);
|
||||
ring.swap_respecting_locks(0, 1);
|
||||
assert_eq!(vals(&ring), vec![1, 0, 2, 3]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
ring.swap_respecting_locks(0, 1);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
|
||||
// Both locked
|
||||
let mut ring = test_deque(&[(0, true), (1, false), (2, true)]);
|
||||
ring.swap_respecting_locks(0, 2);
|
||||
assert_eq!(vals(&ring), vec![2, 1, 0]);
|
||||
assert!(ring[0].locked);
|
||||
assert!(!ring[1].locked);
|
||||
assert!(ring[2].locked);
|
||||
|
||||
// Both unlocked
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false)]);
|
||||
ring.swap_respecting_locks(0, 2);
|
||||
assert_eq!(vals(&ring), vec![2, 1, 0]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
assert!(!ring[2].locked);
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,36 +10,29 @@
|
||||
use std::env::temp_dir;
|
||||
use std::net::Shutdown;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::bail;
|
||||
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::animation::AnimationEngine;
|
||||
use komorebi::replace_env_in_path;
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use sysinfo::Process;
|
||||
use sysinfo::ProcessesToUpdate;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use komorebi::CUSTOM_FFM;
|
||||
use komorebi::DATA_DIR;
|
||||
use komorebi::HOME_DIR;
|
||||
use komorebi::INITIAL_CONFIGURATION_LOADED;
|
||||
use komorebi::SESSION_ID;
|
||||
use komorebi::border_manager;
|
||||
use komorebi::focus_manager;
|
||||
use komorebi::load_configuration;
|
||||
@@ -57,29 +50,32 @@ use komorebi::window_manager::State;
|
||||
use komorebi::window_manager::WindowManager;
|
||||
use komorebi::windows_api::WindowsApi;
|
||||
use komorebi::winevent_listener;
|
||||
use komorebi::CUSTOM_FFM;
|
||||
use komorebi::DATA_DIR;
|
||||
use komorebi::HOME_DIR;
|
||||
use komorebi::INITIAL_CONFIGURATION_LOADED;
|
||||
use komorebi::SESSION_ID;
|
||||
|
||||
fn setup(log_level: LogLevel) -> eyre::Result<(WorkerGuard, WorkerGuard)> {
|
||||
shadow_rs::shadow!(build);
|
||||
|
||||
fn setup(log_level: LogLevel) -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
match log_level {
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Warn => "warn",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Trace => "trace",
|
||||
},
|
||||
);
|
||||
}
|
||||
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");
|
||||
@@ -138,22 +134,20 @@ fn setup(log_level: LogLevel) -> eyre::Result<(WorkerGuard, WorkerGuard)> {
|
||||
#[tracing::instrument]
|
||||
fn detect_deadlocks() {
|
||||
// Create a background thread which checks for deadlocks every 10s
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
tracing::info!("running deadlock detector");
|
||||
std::thread::sleep(Duration::from_secs(5));
|
||||
let deadlocks = deadlock::check_deadlock();
|
||||
if deadlocks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
std::thread::spawn(move || loop {
|
||||
tracing::info!("running deadlock detector");
|
||||
std::thread::sleep(Duration::from_secs(5));
|
||||
let deadlocks = deadlock::check_deadlock();
|
||||
if deadlocks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::error!("{} deadlocks detected", deadlocks.len());
|
||||
for (i, threads) in deadlocks.iter().enumerate() {
|
||||
tracing::error!("deadlock #{}", i);
|
||||
for t in threads {
|
||||
tracing::error!("thread id: {:#?}", t.thread_id());
|
||||
tracing::error!("{:#?}", t.backtrace());
|
||||
}
|
||||
tracing::error!("{} deadlocks detected", deadlocks.len());
|
||||
for (i, threads) in deadlocks.iter().enumerate() {
|
||||
tracing::error!("deadlock #{}", i);
|
||||
for t in threads {
|
||||
tracing::error!("thread id: {:#?}", t.thread_id());
|
||||
tracing::error!("{:#?}", t.backtrace());
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -171,7 +165,7 @@ enum LogLevel {
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version = komorebi::build::CLAP_LONG_VERSION)]
|
||||
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
||||
struct Opts {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(short, long = "ffm")]
|
||||
@@ -184,7 +178,6 @@ 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)]
|
||||
@@ -196,30 +189,12 @@ struct Opts {
|
||||
|
||||
#[tracing::instrument]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn main() -> eyre::Result<()> {
|
||||
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();
|
||||
while set_foreground_window_retries > 0 && !set_foreground_window_succeeded {
|
||||
match WindowsApi::allow_set_foreground_window(process_id) {
|
||||
Ok(_) => {
|
||||
set_foreground_window_succeeded = true;
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
set_foreground_window_retries -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if set_foreground_window_retries == 0 {
|
||||
bail!("failed call to AllowSetForegroundWindow after 5 retries");
|
||||
}
|
||||
}
|
||||
|
||||
WindowsApi::allow_set_foreground_window(process_id)?;
|
||||
WindowsApi::set_process_dpi_awareness_context()?;
|
||||
|
||||
let session_id = WindowsApi::process_id_to_session_id()?;
|
||||
@@ -233,17 +208,15 @@ fn main() -> eyre::Result<()> {
|
||||
if matched_procs.len() > 1 {
|
||||
let mut len = matched_procs.len();
|
||||
for proc in matched_procs {
|
||||
if let Some(executable_path) = proc.exe()
|
||||
&& executable_path.to_string_lossy().contains("shims")
|
||||
{
|
||||
len -= 1;
|
||||
if let Some(executable_path) = proc.exe() {
|
||||
if executable_path.to_string_lossy().contains("shims") {
|
||||
len -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len > 1 {
|
||||
tracing::error!(
|
||||
"komorebi.exe is already running, please exit the existing process before starting a new one"
|
||||
);
|
||||
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,52 +2,64 @@ use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::OptionExt;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::Result;
|
||||
use getset::CopyGetters;
|
||||
use getset::Getters;
|
||||
use getset::MutGetters;
|
||||
use getset::Setters;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::border_manager::BORDER_ENABLED;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::core::Rect;
|
||||
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::DefaultLayout;
|
||||
use crate::FloatingLayerBehaviour;
|
||||
use crate::Layout;
|
||||
use crate::OperationDirection;
|
||||
use crate::Wallpaper;
|
||||
use crate::WindowsApi;
|
||||
use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::workspace::WorkspaceGlobals;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Layout;
|
||||
use crate::OperationDirection;
|
||||
use crate::WindowsApi;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(
|
||||
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Monitor {
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub id: isize,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub name: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub device: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub device_id: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub serial_number_id: Option<String>,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub size: Rect,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub work_area_size: Rect,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub work_area_offset: Option<Rect>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub window_based_work_area_offset: Option<Rect>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub window_based_work_area_offset_limit: isize,
|
||||
pub workspaces: Ring<Workspace>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub last_focused_workspace: Option<usize>,
|
||||
#[getset(get_mut = "pub")]
|
||||
pub workspace_names: HashMap<usize, String>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub container_padding: Option<i32>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub workspace_padding: Option<i32>,
|
||||
pub wallpaper: Option<Wallpaper>,
|
||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||
}
|
||||
|
||||
impl_ring_elements!(Monitor, Workspace);
|
||||
@@ -103,8 +115,6 @@ pub fn new(
|
||||
workspace_names: HashMap::default(),
|
||||
container_padding: None,
|
||||
workspace_padding: None,
|
||||
wallpaper: None,
|
||||
floating_layer_behaviour: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,34 +156,20 @@ impl Monitor {
|
||||
workspace_names: Default::default(),
|
||||
container_padding: None,
|
||||
workspace_padding: None,
|
||||
wallpaper: None,
|
||||
floating_layer_behaviour: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused_workspace_name(&self) -> Option<String> {
|
||||
self.focused_workspace()
|
||||
.map(|w| w.name.clone())
|
||||
.map(|w| w.name().clone())
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
pub fn focused_workspace_layout(&self) -> Option<Layout> {
|
||||
self.focused_workspace().and_then(|workspace| {
|
||||
if workspace.tile {
|
||||
Some(workspace.layout.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> eyre::Result<()> {
|
||||
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, hmonitor, &monitor_wp)?;
|
||||
workspace.restore(mouse_follows_focus)?;
|
||||
} else {
|
||||
workspace.hide(None);
|
||||
}
|
||||
@@ -185,78 +181,46 @@ impl Monitor {
|
||||
/// Updates the `globals` field of all workspaces
|
||||
pub fn update_workspaces_globals(&mut self, offset: Option<Rect>) {
|
||||
let container_padding = self
|
||||
.container_padding
|
||||
.container_padding()
|
||||
.or(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
|
||||
let workspace_padding = self
|
||||
.workspace_padding
|
||||
.workspace_padding()
|
||||
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
|
||||
let (border_width, border_offset) = {
|
||||
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
|
||||
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 work_area_offset = self.work_area_offset.or(offset);
|
||||
let window_based_work_area_offset = self.window_based_work_area_offset;
|
||||
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit;
|
||||
let floating_layer_behaviour = self.floating_layer_behaviour;
|
||||
let work_area = *self.work_area_size();
|
||||
let 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();
|
||||
|
||||
for workspace in self.workspaces_mut() {
|
||||
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,
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the `globals` field of workspace with index `workspace_idx`
|
||||
pub fn update_workspace_globals(&mut self, workspace_idx: usize, offset: Option<Rect>) {
|
||||
let container_padding = self
|
||||
.container_padding
|
||||
.container_padding()
|
||||
.or(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
|
||||
let workspace_padding = self
|
||||
.workspace_padding
|
||||
.workspace_padding()
|
||||
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
|
||||
let (border_width, border_offset) = {
|
||||
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
|
||||
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 work_area_offset = self.work_area_offset.or(offset);
|
||||
let window_based_work_area_offset = self.window_based_work_area_offset;
|
||||
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit;
|
||||
let floating_layer_behaviour = self.floating_layer_behaviour;
|
||||
let work_area = *self.work_area_size();
|
||||
let 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();
|
||||
|
||||
if let Some(workspace) = self.workspaces_mut().get_mut(workspace_idx) {
|
||||
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,
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,14 +228,14 @@ impl Monitor {
|
||||
&mut self,
|
||||
container: Container,
|
||||
workspace_idx: Option<usize>,
|
||||
) -> eyre::Result<()> {
|
||||
) -> Result<()> {
|
||||
let workspace = if let Some(idx) = workspace_idx {
|
||||
self.workspaces_mut()
|
||||
.get_mut(idx)
|
||||
.ok_or_eyre(format!("there is no workspace at index {idx}"))?
|
||||
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
|
||||
} else {
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_eyre("there is no workspace")?
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
};
|
||||
|
||||
workspace.add_container_to_back(container);
|
||||
@@ -288,21 +252,21 @@ impl Monitor {
|
||||
container: Container,
|
||||
workspace_idx: Option<usize>,
|
||||
direction: OperationDirection,
|
||||
) -> eyre::Result<()> {
|
||||
) -> Result<()> {
|
||||
let workspace = if let Some(idx) = workspace_idx {
|
||||
self.workspaces_mut()
|
||||
.get_mut(idx)
|
||||
.ok_or_eyre(format!("there is no workspace at index {idx}"))?
|
||||
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
|
||||
} else {
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_eyre("there is no workspace")?
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
};
|
||||
|
||||
match direction {
|
||||
OperationDirection::Left => {
|
||||
// insert the container into the workspace on the monitor at the back (or rightmost position)
|
||||
// if we are moving across a boundary to the left (back = right side of the target)
|
||||
match workspace.layout {
|
||||
match workspace.layout() {
|
||||
Layout::Default(layout) => match layout {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
workspace.add_container_to_front(container);
|
||||
@@ -326,7 +290,7 @@ impl Monitor {
|
||||
OperationDirection::Right => {
|
||||
// insert the container into the workspace on the monitor at the front (or leftmost position)
|
||||
// if we are moving across a boundary to the right (front = left side of the target)
|
||||
match workspace.layout {
|
||||
match workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index = layout.leftmost_index(workspace.containers().len());
|
||||
|
||||
@@ -390,12 +354,12 @@ impl Monitor {
|
||||
target_workspace_idx: usize,
|
||||
follow: bool,
|
||||
direction: Option<OperationDirection>,
|
||||
) -> eyre::Result<()> {
|
||||
) -> Result<()> {
|
||||
let workspace = self
|
||||
.focused_workspace_mut()
|
||||
.ok_or_eyre("there is no workspace")?;
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?;
|
||||
|
||||
if workspace.maximized_window.is_some() {
|
||||
if workspace.maximized_window().is_some() {
|
||||
bail!("cannot move native maximized window to another monitor or workspace");
|
||||
}
|
||||
|
||||
@@ -418,12 +382,12 @@ impl Monitor {
|
||||
};
|
||||
|
||||
target_workspace.floating_windows_mut().push_back(window);
|
||||
target_workspace.layer = WorkspaceLayer::Floating;
|
||||
target_workspace.set_layer(WorkspaceLayer::Floating);
|
||||
}
|
||||
} else {
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_eyre("there is no container")?;
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
|
||||
@@ -436,19 +400,7 @@ 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.layer = WorkspaceLayer::Tiling;
|
||||
target_workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
|
||||
if let Some(direction) = direction {
|
||||
self.add_container_with_direction(
|
||||
@@ -469,7 +421,7 @@ impl Monitor {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn focus_workspace(&mut self, idx: usize) -> eyre::Result<()> {
|
||||
pub fn focus_workspace(&mut self, idx: usize) -> Result<()> {
|
||||
tracing::info!("focusing workspace");
|
||||
|
||||
{
|
||||
@@ -478,7 +430,7 @@ impl Monitor {
|
||||
if workspaces.get(idx).is_none() {
|
||||
workspaces.resize(idx + 1, Workspace::default());
|
||||
}
|
||||
self.last_focused_workspace = Some(self.workspaces.focused_idx());
|
||||
|
||||
self.workspaces.focus(idx);
|
||||
}
|
||||
|
||||
@@ -488,8 +440,8 @@ impl Monitor {
|
||||
if name.is_some() {
|
||||
self.workspaces_mut()
|
||||
.get_mut(idx)
|
||||
.ok_or_eyre("there is no workspace")?
|
||||
.name = name;
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
.set_name(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,9 +452,9 @@ impl Monitor {
|
||||
self.workspaces().len()
|
||||
}
|
||||
|
||||
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> eyre::Result<()> {
|
||||
let offset = if self.work_area_offset.is_some() {
|
||||
self.work_area_offset
|
||||
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> Result<()> {
|
||||
let offset = if self.work_area_offset().is_some() {
|
||||
self.work_area_offset()
|
||||
} else {
|
||||
offset
|
||||
};
|
||||
@@ -510,7 +462,7 @@ impl Monitor {
|
||||
let focused_workspace_idx = self.focused_workspace_idx();
|
||||
self.update_workspace_globals(focused_workspace_idx, offset);
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_eyre("there is no workspace")?
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
.update()?;
|
||||
|
||||
Ok(())
|
||||
@@ -609,25 +561,6 @@ mod tests {
|
||||
assert_eq!(m.workspaces().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_nonexistent_workspace() {
|
||||
let mut m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Try to remove a workspace that doesn't exist
|
||||
let removed_workspace = m.remove_workspace_by_idx(1);
|
||||
|
||||
// Should return None since there is no workspace at index 1
|
||||
assert!(removed_workspace.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focus_workspace() {
|
||||
let mut m = Monitor::new(
|
||||
@@ -670,157 +603,4 @@ mod tests {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_DISPLAY_ADAPTER;
|
||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_MONITOR;
|
||||
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_VIDEO_OUTPUT_ARRIVAL;
|
||||
@@ -10,6 +11,10 @@ use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::System::Power::POWERBROADCAST_SETTING;
|
||||
use windows::Win32::System::SystemServices::GUID_LIDSWITCH_STATE_CHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DBT_CONFIGCHANGED;
|
||||
@@ -18,9 +23,6 @@ use windows::Win32::UI::WindowsAndMessaging::DBT_DEVICEREMOVECOMPLETE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVTYP_DEVICEINTERFACE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DEV_BROADCAST_DEVICEINTERFACE_W;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND;
|
||||
@@ -28,7 +30,6 @@ use windows::Win32::UI::WindowsAndMessaging::PBT_APMSUSPEND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PBT_POWERSETTINGCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::REGISTER_NOTIFICATION_FLAGS;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_POWERBROADCAST;
|
||||
@@ -37,11 +38,10 @@ use windows::Win32::UI::WindowsAndMessaging::WM_WTSSESSION_CHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
|
||||
use windows::core::PCWSTR;
|
||||
|
||||
use crate::WindowsApi;
|
||||
use crate::monitor_reconciliator;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
|
||||
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -224,18 +224,14 @@ impl Hidden {
|
||||
WM_WTSSESSION_CHANGE => {
|
||||
match wparam.0 as u32 {
|
||||
WTS_SESSION_LOCK => {
|
||||
tracing::debug!(
|
||||
"WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked"
|
||||
);
|
||||
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked");
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::MonitorNotification::SessionLocked,
|
||||
);
|
||||
}
|
||||
WTS_SESSION_UNLOCK => {
|
||||
tracing::debug!(
|
||||
"WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked"
|
||||
);
|
||||
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked");
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::MonitorNotification::SessionUnlocked,
|
||||
@@ -255,8 +251,7 @@ impl Hidden {
|
||||
// and resolution changes here
|
||||
WM_DISPLAYCHANGE => {
|
||||
tracing::debug!(
|
||||
"WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed",
|
||||
wparam.0
|
||||
"WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed", wparam.0
|
||||
);
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
@@ -270,8 +265,8 @@ impl Hidden {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
if wparam.0 as u32 == SPI_SETWORKAREA.0 {
|
||||
tracing::debug!(
|
||||
"WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)"
|
||||
);
|
||||
"WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)"
|
||||
);
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::MonitorNotification::WorkAreaChanged,
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use crate::border_manager;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::core::Rect;
|
||||
@@ -15,6 +7,14 @@ use crate::monitor;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::monitor_reconciliator::hidden::Hidden;
|
||||
use crate::notify_subscribers;
|
||||
use crate::Notification;
|
||||
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;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
@@ -22,10 +22,10 @@ use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub mod hidden;
|
||||
|
||||
@@ -70,10 +70,10 @@ 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.eq(&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()
|
||||
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()
|
||||
};
|
||||
@@ -100,12 +100,12 @@ where
|
||||
}
|
||||
|
||||
for d in &all_displays {
|
||||
if let Some(id) = &d.serial_number_id
|
||||
&& 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());
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,18 +155,16 @@ 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(), win32_display_data::connected_displays_all) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!("restarting failed thread: {:?}", error)
|
||||
} else {
|
||||
tracing::error!("restarting failed thread: {}", error)
|
||||
}
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone(), win32_display_data::connected_displays_all) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!("restarting failed thread: {:?}", error)
|
||||
} else {
|
||||
tracing::error!("restarting failed thread: {}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,27 +219,27 @@ where
|
||||
let mut should_update = false;
|
||||
|
||||
// Update work areas as necessary
|
||||
if let Ok(reference) = WindowsApi::monitor(monitor.id)
|
||||
&& reference.work_area_size != monitor.work_area_size
|
||||
{
|
||||
monitor.work_area_size = Rect {
|
||||
left: reference.work_area_size.left,
|
||||
top: reference.work_area_size.top,
|
||||
right: reference.work_area_size.right,
|
||||
bottom: reference.work_area_size.bottom,
|
||||
};
|
||||
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
|
||||
if reference.work_area_size() != monitor.work_area_size() {
|
||||
monitor.set_work_area_size(Rect {
|
||||
left: reference.work_area_size().left,
|
||||
top: reference.work_area_size().top,
|
||||
right: reference.work_area_size().right,
|
||||
bottom: reference.work_area_size().bottom,
|
||||
});
|
||||
|
||||
should_update = true;
|
||||
should_update = true;
|
||||
}
|
||||
}
|
||||
|
||||
if should_update {
|
||||
tracing::info!("updated work area for {}", monitor.device_id);
|
||||
tracing::info!("updated work area for {}", monitor.device_id());
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
border_manager::send_notification(None);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"work areas match, reconciliation not required for {}",
|
||||
monitor.device_id
|
||||
monitor.device_id()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -253,25 +251,25 @@ where
|
||||
let mut should_update = false;
|
||||
|
||||
// Update sizes and work areas as necessary
|
||||
if let Ok(reference) = WindowsApi::monitor(monitor.id) {
|
||||
if reference.work_area_size != monitor.work_area_size {
|
||||
monitor.work_area_size = Rect {
|
||||
left: reference.work_area_size.left,
|
||||
top: reference.work_area_size.top,
|
||||
right: reference.work_area_size.right,
|
||||
bottom: reference.work_area_size.bottom,
|
||||
};
|
||||
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
|
||||
if reference.work_area_size() != monitor.work_area_size() {
|
||||
monitor.set_work_area_size(Rect {
|
||||
left: reference.work_area_size().left,
|
||||
top: reference.work_area_size().top,
|
||||
right: reference.work_area_size().right,
|
||||
bottom: reference.work_area_size().bottom,
|
||||
});
|
||||
|
||||
should_update = true;
|
||||
}
|
||||
|
||||
if reference.size != monitor.size {
|
||||
monitor.size = Rect {
|
||||
left: reference.size.left,
|
||||
top: reference.size.top,
|
||||
right: reference.size.right,
|
||||
bottom: reference.size.bottom,
|
||||
};
|
||||
if reference.size() != monitor.size() {
|
||||
monitor.set_size(Rect {
|
||||
left: reference.size().left,
|
||||
top: reference.size().top,
|
||||
right: reference.size().right,
|
||||
bottom: reference.size().bottom,
|
||||
});
|
||||
|
||||
should_update = true;
|
||||
}
|
||||
@@ -280,7 +278,7 @@ where
|
||||
if should_update {
|
||||
tracing::info!(
|
||||
"updated monitor resolution/scaling for {}",
|
||||
monitor.device_id
|
||||
monitor.device_id()
|
||||
);
|
||||
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
@@ -288,7 +286,7 @@ where
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"resolutions match, reconciliation not required for {}",
|
||||
monitor.device_id
|
||||
monitor.device_id()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -313,21 +311,21 @@ where
|
||||
for monitor in wm.monitors_mut() {
|
||||
for attached in &attached_devices {
|
||||
let serial_number_ids_match = if let (Some(attached_snid), Some(m_snid)) =
|
||||
(&attached.serial_number_id, &monitor.serial_number_id)
|
||||
(attached.serial_number_id(), monitor.serial_number_id())
|
||||
{
|
||||
attached_snid.eq(m_snid)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if serial_number_ids_match || attached.device_id.eq(&monitor.device_id) {
|
||||
monitor.id = attached.id;
|
||||
monitor.device = attached.device.clone();
|
||||
monitor.device_id = attached.device_id.clone();
|
||||
monitor.serial_number_id = attached.serial_number_id.clone();
|
||||
monitor.name = attached.name.clone();
|
||||
monitor.size = attached.size;
|
||||
monitor.work_area_size = attached.work_area_size;
|
||||
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());
|
||||
monitor.set_serial_number_id(attached.serial_number_id().clone());
|
||||
monitor.set_name(attached.name().clone());
|
||||
monitor.set_size(*attached.size());
|
||||
monitor.set_work_area_size(*attached.work_area_size());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,13 +359,13 @@ where
|
||||
|
||||
for (m_idx, m) in wm.monitors().iter().enumerate() {
|
||||
if !attached_devices.iter().any(|attached| {
|
||||
attached.serial_number_id.eq(&m.serial_number_id)
|
||||
|| attached.device_id.eq(&m.device_id)
|
||||
attached.serial_number_id().eq(m.serial_number_id())
|
||||
|| attached.device_id().eq(m.device_id())
|
||||
}) {
|
||||
let id = m
|
||||
.serial_number_id
|
||||
.serial_number_id()
|
||||
.as_ref()
|
||||
.map_or(m.device_id.clone(), |sn| sn.clone());
|
||||
.map_or(m.device_id().clone(), |sn| sn.clone());
|
||||
|
||||
newly_removed_displays.push(id.clone());
|
||||
|
||||
@@ -394,7 +392,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(maximized) = &workspace.maximized_window {
|
||||
if let Some(maximized) = workspace.maximized_window() {
|
||||
windows_to_remove.push(maximized.hwnd);
|
||||
// Minimize the focused window since Windows might try
|
||||
// to move it to another monitor if it was focused.
|
||||
@@ -403,7 +401,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(container) = &workspace.monocle_container {
|
||||
if let Some(container) = workspace.monocle_container() {
|
||||
for window in container.windows() {
|
||||
windows_to_remove.push(window.hwnd);
|
||||
}
|
||||
@@ -442,10 +440,10 @@ where
|
||||
// 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.eq(&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()
|
||||
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
|
||||
};
|
||||
@@ -460,8 +458,8 @@ where
|
||||
// After we have cached them, remove them from our state
|
||||
wm.monitors_mut().retain(|m| {
|
||||
!newly_removed_displays.iter().any(|id| {
|
||||
m.serial_number_id.as_ref().is_some_and(|sn| sn == id)
|
||||
|| m.device_id.eq(id)
|
||||
m.serial_number_id().as_ref().is_some_and(|sn| sn == id)
|
||||
|| m.device_id() == id
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -492,7 +490,7 @@ where
|
||||
let post_removal_device_ids = wm
|
||||
.monitors()
|
||||
.iter()
|
||||
.map(|m| &m.device_id)
|
||||
.map(Monitor::device_id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -515,23 +513,21 @@ where
|
||||
|
||||
// Look in the updated state for new monitors
|
||||
for (i, m) in wm.monitors_mut().iter_mut().enumerate() {
|
||||
let device_id = &m.device_id;
|
||||
let device_id = m.device_id();
|
||||
// We identify a new monitor when we encounter a new device id
|
||||
if !post_removal_device_ids.contains(device_id) {
|
||||
let mut cache_hit = false;
|
||||
let mut cached_id = String::new();
|
||||
// Check if that device id exists in the cache for this session
|
||||
if let Some((id, cached)) = monitor_cache.get_key_value(device_id).or(m
|
||||
.serial_number_id
|
||||
.serial_number_id()
|
||||
.as_ref()
|
||||
.and_then(|sn| monitor_cache.get_key_value(sn)))
|
||||
{
|
||||
cache_hit = true;
|
||||
cached_id = id.clone();
|
||||
|
||||
tracing::info!(
|
||||
"found monitor and workspace configuration for {id} in the monitor cache, applying"
|
||||
);
|
||||
tracing::info!("found monitor and workspace configuration for {id} in the monitor cache, applying");
|
||||
|
||||
// If it does, update the cached monitor info with the new one and
|
||||
// load the cached monitor removing any window that has since been
|
||||
@@ -557,8 +553,6 @@ where
|
||||
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();
|
||||
@@ -612,24 +606,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = &workspace.maximized_window {
|
||||
if let Some(window) = workspace.maximized_window() {
|
||||
if window.exe().is_err()
|
||||
|| known_hwnds.contains_key(&window.hwnd)
|
||||
{
|
||||
workspace.maximized_window = None;
|
||||
workspace.set_maximized_window(None);
|
||||
} else if is_focused_workspace {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(container) = &mut workspace.monocle_container {
|
||||
if let Some(container) = workspace.monocle_container_mut() {
|
||||
container.windows_mut().retain(|window| {
|
||||
window.exe().is_ok()
|
||||
&& !known_hwnds.contains_key(&window.hwnd)
|
||||
});
|
||||
|
||||
if container.windows().is_empty() {
|
||||
workspace.monocle_container = None;
|
||||
workspace.set_monocle_container(None);
|
||||
} else if is_focused_workspace {
|
||||
if let Some(window) = container.focused_window() {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
@@ -661,7 +655,7 @@ where
|
||||
let mut workspace_matching_rules =
|
||||
WORKSPACE_MATCHING_RULES.lock();
|
||||
if let Some(rules) = workspace
|
||||
.workspace_config
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.workspace_rules.as_ref())
|
||||
{
|
||||
@@ -676,7 +670,7 @@ where
|
||||
}
|
||||
|
||||
if let Some(rules) = workspace
|
||||
.workspace_config
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.initial_workspace_rules.as_ref())
|
||||
{
|
||||
@@ -738,8 +732,8 @@ where
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_channel::bounded;
|
||||
use crossbeam_channel::Sender;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
use windows::Win32::Devices::Display::DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY;
|
||||
@@ -805,7 +799,7 @@ mod tests {
|
||||
let wm = match WindowManager::new(receiver, Some(socket_path.clone())) {
|
||||
Ok(manager) => manager,
|
||||
Err(e) => {
|
||||
panic!("Failed to create WindowManager: {e}");
|
||||
panic!("Failed to create WindowManager: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -833,7 +827,7 @@ mod tests {
|
||||
Ok(notification) => {
|
||||
assert_eq!(notification, MonitorNotification::ResolutionScalingChanged);
|
||||
}
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -853,7 +847,7 @@ mod tests {
|
||||
for _ in 0..20 {
|
||||
let notification = match receiver.try_recv() {
|
||||
Ok(notification) => notification,
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
};
|
||||
assert_eq!(
|
||||
notification,
|
||||
@@ -964,7 +958,7 @@ mod tests {
|
||||
Ok(notification) => {
|
||||
assert_eq!(notification, MonitorNotification::DisplayConnectionChange);
|
||||
}
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1010,18 +1004,18 @@ mod tests {
|
||||
assert_eq!(monitors.len(), 1, "Expected one monitor");
|
||||
|
||||
// hmonitor
|
||||
assert_eq!(monitors[0].id, 1);
|
||||
assert_eq!(monitors[0].id(), 1);
|
||||
|
||||
// device name
|
||||
assert_eq!(monitors[0].name, String::from("DISPLAY1"));
|
||||
assert_eq!(monitors[0].name(), &String::from("DISPLAY1"));
|
||||
|
||||
// Device
|
||||
assert_eq!(monitors[0].device, String::from("ABC123"));
|
||||
assert_eq!(monitors[0].device(), &String::from("ABC123"));
|
||||
|
||||
// Device ID
|
||||
assert_eq!(
|
||||
monitors[0].device_id,
|
||||
String::from("ABC123-4&123456&0&UID0")
|
||||
monitors[0].device_id(),
|
||||
&String::from("ABC123-4&123456&0&UID0")
|
||||
);
|
||||
|
||||
// Check monitor serial number id
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::OptionExt;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
@@ -11,18 +11,6 @@ use crate::core::Rect;
|
||||
use crate::core::Sizing;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
|
||||
use crate::CURRENT_VIRTUAL_DESKTOP;
|
||||
use crate::DefaultLayout;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::Layout;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::State;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::VirtualDesktopNotification;
|
||||
use crate::Window;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
@@ -30,13 +18,21 @@ use crate::current_virtual_desktop;
|
||||
use crate::notify_subscribers;
|
||||
use crate::stackbar_manager;
|
||||
use crate::transparency_manager;
|
||||
use crate::window::RuleDebug;
|
||||
use crate::window::should_act;
|
||||
use crate::window::RuleDebug;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::Window;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||
@@ -65,7 +61,7 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||
impl WindowManager {
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
#[tracing::instrument(skip(self, event), fields(event = event.title(), winevent = event.winevent(), hwnd = event.hwnd()))]
|
||||
pub fn process_event(&mut self, event: WindowManagerEvent) -> eyre::Result<()> {
|
||||
pub fn process_event(&mut self, event: WindowManagerEvent) -> Result<()> {
|
||||
if self.is_paused {
|
||||
tracing::trace!("ignoring while paused");
|
||||
return Ok(());
|
||||
@@ -120,66 +116,15 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
let mut last_known_virtual_desktop_id = CURRENT_VIRTUAL_DESKTOP.lock();
|
||||
|
||||
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
|
||||
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 let Some(id) = current_virtual_desktop() {
|
||||
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,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,11 +152,12 @@ impl WindowManager {
|
||||
//
|
||||
// This check ensures that we only update the focused monitor when the window
|
||||
// triggering monitor reconciliation is known to not be tied to a specific monitor.
|
||||
if let Ok(class) = window.class()
|
||||
&& class != "OleMainThreadWndClass"
|
||||
&& self.focused_monitor_idx() != monitor_idx
|
||||
{
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
if let Ok(class) = window.class() {
|
||||
if class != "OleMainThreadWndClass"
|
||||
&& self.focused_monitor_idx() != monitor_idx
|
||||
{
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,11 +252,7 @@ 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 !matches!(
|
||||
self.focused_workspace()?.layout,
|
||||
Layout::Default(DefaultLayout::Scrolling)
|
||||
) && !self.focused_workspace()?.containers().is_empty()
|
||||
{
|
||||
if !self.focused_workspace()?.containers().is_empty() {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
}
|
||||
|
||||
@@ -322,13 +264,13 @@ impl WindowManager {
|
||||
|
||||
match floating_window_idx {
|
||||
None => {
|
||||
if let Some(w) = &workspace.maximized_window
|
||||
&& w.hwnd == window.hwnd
|
||||
{
|
||||
return Ok(());
|
||||
if let Some(w) = workspace.maximized_window() {
|
||||
if w.hwnd == window.hwnd {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(monocle) = &workspace.monocle_container {
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
window.focus(false)?;
|
||||
}
|
||||
@@ -336,19 +278,11 @@ impl WindowManager {
|
||||
workspace.focus_container_by_window(window.hwnd)?;
|
||||
}
|
||||
|
||||
workspace.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)?;
|
||||
}
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
}
|
||||
Some(idx) => {
|
||||
if let Some(_window) = workspace.floating_windows().get(idx) {
|
||||
workspace.layer = WorkspaceLayer::Floating;
|
||||
workspace.set_layer(WorkspaceLayer::Floating);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,8 +296,8 @@ impl WindowManager {
|
||||
tracing::info!("ignoring uncloak after monocle move by mouse across monitors");
|
||||
self.uncloack_to_ignore = self.uncloack_to_ignore.saturating_sub(1);
|
||||
} else {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
let mut focused_monitor_idx = self.focused_monitor_idx();
|
||||
let mut focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
|
||||
let mut needs_reconciliation = None;
|
||||
@@ -392,30 +326,45 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd)
|
||||
&& let Some(focused_workspace_idx) = self
|
||||
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if let Some(focused_workspace_idx) = self
|
||||
.monitors()
|
||||
.get(*m_idx)
|
||||
.map(|m| m.focused_workspace_idx())
|
||||
&& *m_idx != self.focused_monitor_idx()
|
||||
&& *w_idx != focused_workspace_idx
|
||||
{
|
||||
tracing::debug!(
|
||||
"ignoring show event for window already associated with another workspace"
|
||||
);
|
||||
{
|
||||
if *m_idx != self.focused_monitor_idx()
|
||||
&& *w_idx != focused_workspace_idx
|
||||
{
|
||||
tracing::debug!(
|
||||
"ignoring show event for window already associated with another workspace"
|
||||
);
|
||||
|
||||
window.hide();
|
||||
proceed = false;
|
||||
window.hide();
|
||||
proceed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if proceed {
|
||||
let behaviour = self.window_management_behaviour(
|
||||
if matches!(event, WindowManagerEvent::Show(_, _)) {
|
||||
let initial_monitor_idx = initial_state.monitors.focused_idx();
|
||||
if focused_monitor_idx != initial_monitor_idx {
|
||||
tracing::info!("assuming focused monitor index should be {initial_monitor_idx} for WindowManagerEvent::Show");
|
||||
self.focus_monitor(initial_monitor_idx)?;
|
||||
}
|
||||
|
||||
focused_monitor_idx = self.focused_monitor_idx();
|
||||
focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
}
|
||||
|
||||
let mut behaviour = self.window_management_behaviour(
|
||||
focused_monitor_idx,
|
||||
focused_workspace_idx,
|
||||
);
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let workspace_contains_window = workspace.contains_window(window.hwnd);
|
||||
let monocle_container = workspace.monocle_container.clone();
|
||||
let monocle_container = workspace.monocle_container().clone();
|
||||
|
||||
if !workspace_contains_window && needs_reconciliation.is_none() {
|
||||
let floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
@@ -439,47 +388,40 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
behaviour.float_override = behaviour.float_override
|
||||
|| (should_float
|
||||
&& !matches!(event, WindowManagerEvent::Manage(_)));
|
||||
|
||||
if behaviour.float_override {
|
||||
// Center floating windows if we are already on the `Floating`
|
||||
// layer and the window doesn't match a `floating_windows` rule and
|
||||
// the workspace is not a floating workspace
|
||||
let center_spawned_floats =
|
||||
placement.should_center() && workspace.tile;
|
||||
matches!(workspace.layer, WorkspaceLayer::Floating)
|
||||
&& !should_float
|
||||
&& workspace.tile;
|
||||
workspace.floating_windows_mut().push_back(window);
|
||||
workspace.layer = WorkspaceLayer::Floating;
|
||||
workspace.set_layer(WorkspaceLayer::Floating);
|
||||
if center_spawned_floats {
|
||||
let mut floating_window = window;
|
||||
floating_window.center(
|
||||
&workspace.globals.work_area,
|
||||
placement.should_resize(),
|
||||
)?;
|
||||
floating_window.center(&workspace.globals().work_area)?;
|
||||
}
|
||||
self.update_focused_workspace(false, false)?;
|
||||
} else {
|
||||
match behaviour.current_behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
workspace.new_container_for_window(window);
|
||||
workspace.layer = WorkspaceLayer::Tiling;
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
workspace
|
||||
.focused_container_mut()
|
||||
.ok_or_eyre("there is no focused container")?
|
||||
.ok_or_else(|| {
|
||||
anyhow!("there is no focused container")
|
||||
})?
|
||||
.add_window(window);
|
||||
workspace.layer = WorkspaceLayer::Tiling;
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
self.update_focused_workspace(true, false)?;
|
||||
stackbar_manager::send_notification();
|
||||
}
|
||||
@@ -504,17 +446,15 @@ impl WindowManager {
|
||||
|
||||
if workspace_contains_window {
|
||||
let mut monocle_window_event = false;
|
||||
if let Some(ref monocle) = monocle_container
|
||||
&& let Some(monocle_window) = monocle.focused_window()
|
||||
&& monocle_window.hwnd == window.hwnd
|
||||
{
|
||||
monocle_window_event = true;
|
||||
if let Some(ref monocle) = monocle_container {
|
||||
if let Some(monocle_window) = monocle.focused_window() {
|
||||
if monocle_window.hwnd == window.hwnd {
|
||||
monocle_window_event = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let workspace = self.focused_workspace()?;
|
||||
if !(monocle_window_event || workspace.layer != WorkspaceLayer::Tiling)
|
||||
&& monocle_container.is_some()
|
||||
{
|
||||
if !monocle_window_event && monocle_container.is_some() {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
@@ -525,7 +465,7 @@ impl WindowManager {
|
||||
let monitor_idx = self.focused_monitor_idx();
|
||||
let workspace_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_eyre("there is no monitor with this idx")?
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace_idx();
|
||||
|
||||
WindowsApi::bring_window_to_top(window.hwnd)?;
|
||||
@@ -543,19 +483,19 @@ impl WindowManager {
|
||||
|
||||
// If the window handles don't match then something went wrong and the pending move
|
||||
// is not related to this current move, if so abort this operation.
|
||||
if let Some((_, _, w_hwnd)) = pending
|
||||
&& w_hwnd != window.hwnd
|
||||
{
|
||||
color_eyre::eyre::bail!(
|
||||
"window handles for move operation don't match: {} != {}",
|
||||
w_hwnd,
|
||||
window.hwnd
|
||||
);
|
||||
if let Some((_, _, w_hwnd)) = pending {
|
||||
if w_hwnd != window.hwnd {
|
||||
color_eyre::eyre::bail!(
|
||||
"window handles for move operation don't match: {} != {}",
|
||||
w_hwnd,
|
||||
window.hwnd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let target_monitor_idx = self
|
||||
.monitor_idx_from_current_pos()
|
||||
.ok_or_eyre("cannot get monitor idx from current position")?;
|
||||
.ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?;
|
||||
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
|
||||
@@ -566,7 +506,7 @@ impl WindowManager {
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
let new_position = WindowsApi::window_rect(window.hwnd)?;
|
||||
let old_position = *workspace
|
||||
.latest_layout
|
||||
.latest_layout()
|
||||
.get(focused_container_idx)
|
||||
// If the move was to another monitor with an empty workspace, the
|
||||
// workspace here will refer to that empty workspace, which won't
|
||||
@@ -578,10 +518,10 @@ impl WindowManager {
|
||||
// This will be true if we have moved to another monitor
|
||||
let mut moved_across_monitors = false;
|
||||
|
||||
if let Some((m_idx, _)) = self.known_hwnds.get(&window.hwnd)
|
||||
&& *m_idx != target_monitor_idx
|
||||
{
|
||||
moved_across_monitors = true;
|
||||
if let Some((m_idx, _)) = self.known_hwnds.get(&window.hwnd) {
|
||||
if *m_idx != target_monitor_idx {
|
||||
moved_across_monitors = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((origin_monitor_idx, origin_workspace_idx, _)) = pending {
|
||||
@@ -599,10 +539,10 @@ impl WindowManager {
|
||||
let origin_workspace = self
|
||||
.monitors()
|
||||
.get(origin_monitor_idx)
|
||||
.ok_or_eyre("cannot get monitor idx")?
|
||||
.ok_or_else(|| anyhow!("cannot get monitor idx"))?
|
||||
.workspaces()
|
||||
.get(origin_workspace_idx)
|
||||
.ok_or_eyre("cannot get workspace idx")?;
|
||||
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
|
||||
|
||||
let managed_window = origin_workspace.contains_window(window.hwnd);
|
||||
|
||||
@@ -614,7 +554,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if (workspace.tile && workspace.contains_managed_window(window.hwnd))
|
||||
if (*workspace.tile() && workspace.contains_managed_window(window.hwnd))
|
||||
|| moved_across_monitors
|
||||
{
|
||||
let resize = Rect {
|
||||
@@ -642,15 +582,17 @@ impl WindowManager {
|
||||
let target_workspace_idx = self
|
||||
.monitors()
|
||||
.get(target_monitor_idx)
|
||||
.ok_or_eyre("there is no monitor at this idx")?
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
|
||||
.focused_workspace_idx();
|
||||
|
||||
let target_container_idx = self
|
||||
.monitors()
|
||||
.get(target_monitor_idx)
|
||||
.ok_or_eyre("there is no monitor at this idx")?
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
|
||||
.focused_workspace()
|
||||
.ok_or_eyre("there is no focused workspace for this monitor")?
|
||||
.ok_or_else(|| {
|
||||
anyhow!("there is no focused workspace for this monitor")
|
||||
})?
|
||||
.container_idx_from_current_point()
|
||||
// Default to 0 in the case of an empty workspace
|
||||
.unwrap_or(0);
|
||||
@@ -670,7 +612,7 @@ impl WindowManager {
|
||||
let origin_monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_eyre("there is no monitor at this idx")?;
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
|
||||
origin_monitor.focus_workspace(origin_workspace_idx)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
|
||||
@@ -678,7 +620,7 @@ impl WindowManager {
|
||||
let target_monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_eyre("there is no monitor at this idx")?;
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
|
||||
target_monitor.focus_workspace(target_workspace_idx)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
|
||||
@@ -779,7 +721,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()?, true)?;
|
||||
window.center(&self.focused_monitor_work_area()?)?;
|
||||
}
|
||||
|
||||
// Update list of known_hwnds and their monitor/workspace index pair
|
||||
@@ -830,7 +772,7 @@ impl WindowManager {
|
||||
.and_then(|m| m.workspaces().get(*ws_idx))
|
||||
{
|
||||
if let Some(monocle_with_window) = target_workspace
|
||||
.monocle_container
|
||||
.monocle_container()
|
||||
.as_ref()
|
||||
.and_then(|m| m.contains_window(window.hwnd).then_some(m))
|
||||
{
|
||||
@@ -843,12 +785,13 @@ impl WindowManager {
|
||||
|
||||
if let Some(target_container) =
|
||||
c_idx.and_then(|c_idx| target_workspace.containers().get(c_idx))
|
||||
&& target_container.focused_window() != Some(&window)
|
||||
{
|
||||
tracing::debug!(
|
||||
"Needs reconciliation within a stack on the focused workspace"
|
||||
);
|
||||
needs_reconciliation = Some((*m_idx, *ws_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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -879,13 +822,13 @@ impl WindowManager {
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
if ws_idx != monitor.focused_workspace_idx() {
|
||||
let previous_idx = monitor.focused_workspace_idx();
|
||||
monitor.last_focused_workspace = Option::from(previous_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
|
||||
.monocle_container_mut()
|
||||
.as_mut()
|
||||
.and_then(|m| m.idx_for_window(window.hwnd).map(|i| (m, i)))
|
||||
{
|
||||
@@ -896,14 +839,14 @@ impl WindowManager {
|
||||
.any(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
layer = WorkspaceLayer::Floating;
|
||||
} else if workspace
|
||||
.maximized_window
|
||||
.is_none_or(|w| w.hwnd != window.hwnd)
|
||||
} 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() {
|
||||
if workspace.monocle_container().is_some() {
|
||||
tracing::info!("disabling monocle");
|
||||
for container in workspace.containers_mut() {
|
||||
container.restore();
|
||||
@@ -915,7 +858,7 @@ impl WindowManager {
|
||||
}
|
||||
workspace.focus_container_by_window(window.hwnd)?;
|
||||
}
|
||||
workspace.layer = layer;
|
||||
workspace.set_layer(layer);
|
||||
}
|
||||
monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user