mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-16 10:17:41 +01:00
Compare commits
65 Commits
feature/fl
...
v0.1.38
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffa76ea28c | ||
|
|
2c00d79968 | ||
|
|
78177af6b8 | ||
|
|
86e0d40828 | ||
|
|
48f6ac8964 | ||
|
|
1db572f789 | ||
|
|
3dad77533b | ||
|
|
f40fb9a251 | ||
|
|
b4e16e43e9 | ||
|
|
4c2e8ff6d2 | ||
|
|
c879aae1e7 | ||
|
|
7e87e83189 | ||
|
|
aae9338f66 | ||
|
|
f68a709f1d | ||
|
|
c76846ac63 | ||
|
|
b29dd8b1d1 | ||
|
|
59c3c14731 | ||
|
|
db96f2cc5a | ||
|
|
a37a6752a8 | ||
|
|
4d0df9c5b5 | ||
|
|
f8ea62f857 | ||
|
|
93bb41737b | ||
|
|
280352eeef | ||
|
|
7619b9b4ed | ||
|
|
72a4d5276e | ||
|
|
4bb3b83d57 | ||
|
|
ccd2f3a464 | ||
|
|
7e242ada66 | ||
|
|
5b2acd0f12 | ||
|
|
ec0bbaae98 | ||
|
|
3c44f3bfdb | ||
|
|
7839980ddf | ||
|
|
6416c0b6eb | ||
|
|
f6ccec9505 | ||
|
|
21cb5e1e6f | ||
|
|
009c0dcd28 | ||
|
|
98c5ab3b9b | ||
|
|
d4eeec994f | ||
|
|
e9ed1cfd3b | ||
|
|
4a2eb391f7 | ||
|
|
41e18bccc6 | ||
|
|
3d373b3630 | ||
|
|
b4e61b079c | ||
|
|
eec6312a51 | ||
|
|
00384ce333 | ||
|
|
e75578d9ce | ||
|
|
50c850cb02 | ||
|
|
5a1af5b133 | ||
|
|
74c433185a | ||
|
|
71bb346c89 | ||
|
|
3feff1dca9 | ||
|
|
3019eaf89c | ||
|
|
ce59bd9ae4 | ||
|
|
309dd159ca | ||
|
|
6f1d6dbdc7 | ||
|
|
6a10d583a6 | ||
|
|
270ea5aa46 | ||
|
|
a9d2738733 | ||
|
|
3d8f68e559 | ||
|
|
80bb7288c4 | ||
|
|
76c833f661 | ||
|
|
80bce4be7e | ||
|
|
8c10547325 | ||
|
|
8f886b3fe4 | ||
|
|
53c38e157f |
2
.github/workflows/feature-check.yaml
vendored
2
.github/workflows/feature-check.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check and close feature issues
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
16
.github/workflows/windows.yaml
vendored
16
.github/workflows/windows.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: rustup toolchain install stable --profile minimal
|
||||
@@ -81,12 +81,12 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
@@ -128,14 +128,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
@@ -170,14 +170,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
- 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
|
||||
|
||||
2090
Cargo.lock
generated
2090
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -9,7 +9,7 @@ members = [
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes",
|
||||
"komorebi-shortcuts"
|
||||
"komorebi-shortcuts",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -33,6 +33,7 @@ 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.34"
|
||||
uds_windows = "1"
|
||||
@@ -51,7 +52,7 @@ features = [
|
||||
"Win32_Devices",
|
||||
"Win32_Devices_Display",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Dwm",
|
||||
@@ -72,5 +73,12 @@ features = [
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control"
|
||||
"Media_Control",
|
||||
]
|
||||
|
||||
[profile.release-opt]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
@@ -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.36"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.38"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
|
||||
@@ -14,7 +14,8 @@ feature-depth = 1
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" }
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
|
||||
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" }
|
||||
]
|
||||
|
||||
[licenses]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
|
||||
|
||||
Arguments:
|
||||
<DEFAULT_LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -10,7 +10,7 @@ Arguments:
|
||||
Target workspace name
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe query <STATE_QUERY>
|
||||
|
||||
Arguments:
|
||||
<STATE_QUERY>
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, version]
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, focused-container-kind, version]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
16
docs/cli/scrolling-layout-columns.md
Normal file
16
docs/cli/scrolling-layout-columns.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# scrolling-layout-columns
|
||||
|
||||
```
|
||||
Set the number of visible columns for the Scrolling layout on the focused workspace
|
||||
|
||||
Usage: komorebic.exe scrolling-layout-columns <COUNT>
|
||||
|
||||
Arguments:
|
||||
<COUNT>
|
||||
Desired number of visible columns
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# toggle-pause
|
||||
|
||||
```
|
||||
Toggle window tiling on the focused workspace
|
||||
Toggle the paused state for all window tiling
|
||||
|
||||
Usage: komorebic.exe toggle-pause
|
||||
|
||||
|
||||
12
docs/cli/toggle-shortcuts.md
Normal file
12
docs/cli/toggle-shortcuts.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-shortcuts
|
||||
|
||||
```
|
||||
Toggle the komorebi-shortcuts helper
|
||||
|
||||
Usage: komorebic.exe toggle-shortcuts
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -8,7 +8,7 @@ Usage: komorebic.exe window-hiding-behaviour <HIDING_BEHAVIOUR>
|
||||
Arguments:
|
||||
<HIDING_BEHAVIOUR>
|
||||
Possible values:
|
||||
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
- hide: END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
31
docs/cli/workspace-work-area-offset.md
Normal file
31
docs/cli/workspace-work-area-offset.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# workspace-work-area-offset
|
||||
|
||||
```
|
||||
Set offsets for a workspace to exclude parts of the work area from tiling
|
||||
|
||||
Usage: komorebic.exe workspace-work-area-offset <MONITOR> <WORKSPACE> <LEFT> <TOP> <RIGHT> <BOTTOM>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index (zero-indexed)
|
||||
|
||||
<LEFT>
|
||||
Size of the left work area offset (set right to left * 2 to maintain right padding)
|
||||
|
||||
<TOP>
|
||||
Size of the top work area offset (set bottom to the same value to maintain bottom padding)
|
||||
|
||||
<RIGHT>
|
||||
Size of the right work area offset
|
||||
|
||||
<BOTTOM>
|
||||
Size of the bottom work area offset
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -6,7 +6,10 @@ defined in the `komorebi.json` configuration file.
|
||||
```json
|
||||
{
|
||||
"animation": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"duration": 250,
|
||||
"fps": 60,
|
||||
"style": "EaseOutSine"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -301,7 +301,7 @@ how to map the indices and would use default behaviour which would result in a m
|
||||
}
|
||||
```
|
||||
|
||||
# Multiple Monitors on different machines
|
||||
# Multiple monitors on different machines
|
||||
|
||||
You can use the same `komorebi.json` to configure two different setups and then synchronize your config across machines.
|
||||
However, if you do this it is important to be aware of a few things.
|
||||
@@ -393,6 +393,13 @@ This is because komorebi will apply the appropriate config to the loaded monitor
|
||||
index (the index defined in the user config) to the actual monitor index, and the bar will use that map to know if it
|
||||
should be enabled, and where it should be drawn.
|
||||
|
||||
# Windows Display Settings
|
||||
|
||||
In `Settings > System > Display > Multiple Displays`:
|
||||
|
||||
- Disable "Remember windows locations on monitor connection"
|
||||
- Enable "Minimize windows when a monitor is disconnected"
|
||||
|
||||
### Things to keep in mind
|
||||
|
||||
* If you are using a laptop connected to one monitor at work and a different one at home, the work monitor and the home
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Setting a Given Display to a Specific Index
|
||||
|
||||
If you would like `komorebi` to remember monitor index positions, you will need to set the `display_index_preferences`
|
||||
configuration option in the static configuration file.
|
||||
|
||||
Display IDs can be found using `komorebic monitor-information`.
|
||||
|
||||
Then, in `komorebi.json`, you simply need to specify the preferred index position for each display ID:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "DEL4310-5&1a6c0954&0&UID209155",
|
||||
"1": "<another-display_id>"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -186,6 +186,9 @@ limitations on hotkey bindings that include the `win` key. However, you will sti
|
||||
to [modify the registry](https://superuser.com/questions/1059511/how-to-disable-winl-in-windows-10) to prevent
|
||||
`win + l` from locking the operating system.
|
||||
|
||||
You can toggle an overlay of the current `whkdrc` shortcuts related to `komorebi` at any time when using the example
|
||||
configuration with `alt + i`.
|
||||
|
||||
```
|
||||
{% include "./whkdrc.sample" %}
|
||||
```
|
||||
|
||||
@@ -120,6 +120,7 @@ cargo +stable install --path komorebic --locked
|
||||
cargo +stable install --path komorebic-no-console --locked
|
||||
cargo +stable install --path komorebi-gui --locked
|
||||
cargo +stable install --path komorebi-bar --locked
|
||||
cargo +stable install --path komorebi-shortcuts --locked
|
||||
```
|
||||
|
||||
If the binaries have been built and added to your `$PATH` correctly, you should
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.36/schema.bar.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.38/schema.bar.json",
|
||||
"monitor": 0,
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.36/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.38/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
|
||||
@@ -138,13 +138,14 @@ running `komorebic stop` and `komorebic start`.
|
||||
Users with Nvidia GPUs may have issues with transparency on the Komorebi Bar.
|
||||
|
||||
To solve this the user can do the following:
|
||||
1. Open the Nvidia Control Panel
|
||||
2. On the left menu tree, under "3D Settings", select "Manage 3D Settings"
|
||||
3. Select the "Program Settings" tab
|
||||
4. Press the "Add" button and select "komorebi-bar"
|
||||
5. Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
|
||||
6. Change the setting to "Prefer compatibility"
|
||||
7. At the bottom of the window select "Apply"
|
||||
8. Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
|
||||
|
||||
- Open the Nvidia Control Panel
|
||||
- On the left menu tree, under "3D Settings", select "Manage 3D Settings"
|
||||
- Select the "Program Settings" tab
|
||||
- Press the "Add" button and select "komorebi-bar"
|
||||
- Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
|
||||
- Change the setting to "Prefer compatibility"
|
||||
- At the bottom of the window select "Apply"
|
||||
- Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
|
||||
|
||||
This should resolve the issue and your Komorebi Bar should render with the proper transparency.
|
||||
|
||||
4
justfile
4
justfile
@@ -28,7 +28,7 @@ install-target-with-jsonschema target:
|
||||
cargo +stable install --path {{ target }} --locked
|
||||
|
||||
install:
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
|
||||
install-with-jsonschema:
|
||||
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
@@ -52,7 +52,7 @@ wpm target:
|
||||
just build-target {{ target }} && wpmctl stop {{ target }}; just copy-target {{ target }} && wpmctl start {{ target }}
|
||||
|
||||
copy:
|
||||
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
|
||||
run target:
|
||||
cargo +stable run --bin {{ target }} --locked --no-default-features
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
komorebi-themes = { path = "../komorebi-themes", default-features = false }
|
||||
|
||||
chrono-tz = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
@@ -22,10 +22,11 @@ font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
lazy_static = { workspace = true }
|
||||
netdev = "0.34"
|
||||
netdev = "0.36"
|
||||
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 }
|
||||
@@ -43,4 +44,4 @@ windows-icons-fallback = { package = "windows-icons", git = "https://github.com/
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["dep:schemars", "komorebi-client/schemars", "komorebi-themes/schemars"]
|
||||
schemars = ["dep:schemars", "komorebi-client/default", "komorebi-themes/default"]
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::render::RenderExt;
|
||||
use crate::widgets::komorebi::Komorebi;
|
||||
use crate::widgets::komorebi::KomorebiNotificationState;
|
||||
use crate::widgets::komorebi::MonitorInfo;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::widgets::widget::WidgetConfig;
|
||||
use crate::KomorebiEvent;
|
||||
@@ -38,6 +38,7 @@ use eframe::egui::Frame;
|
||||
use eframe::egui::Id;
|
||||
use eframe::egui::Layout;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::PointerButton;
|
||||
use eframe::egui::Rgba;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::TextStyle;
|
||||
@@ -46,31 +47,110 @@ use eframe::egui::Visuals;
|
||||
use font_loader::system_fonts;
|
||||
use font_loader::system_fonts::FontPropertyBuilder;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::MonitorNotification;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::VirtualDesktopNotification;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use komorebi_themes::Base16Value;
|
||||
use komorebi_themes::Base16Wrapper;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::CatppuccinValue;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Error;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Result;
|
||||
use std::io::Write;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ChildStdin;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
lazy_static! {
|
||||
static ref SESSION_STDIN: Mutex<Option<ChildStdin>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
fn start_powershell() -> Result<()> {
|
||||
// found running session, do nothing
|
||||
if SESSION_STDIN.lock().as_mut().is_some() {
|
||||
tracing::debug!("PowerShell session already started");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::debug!("Starting PowerShell session");
|
||||
|
||||
let mut child = Command::new("powershell.exe")
|
||||
.args(["-NoLogo", "-NoProfile", "-Command", "-"])
|
||||
.stdin(Stdio::piped())
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().expect("stdin piped");
|
||||
|
||||
// Store stdin for later commands
|
||||
let mut session_stdin = SESSION_STDIN.lock();
|
||||
*session_stdin = Option::from(stdin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_powershell() -> Result<()> {
|
||||
tracing::debug!("Stopping PowerShell session");
|
||||
|
||||
if let Some(mut session_stdin) = SESSION_STDIN.lock().take() {
|
||||
if let Err(e) = session_stdin.write_all(b"exit\n") {
|
||||
tracing::error!(error = %e, "failed to write exit command to PowerShell stdin");
|
||||
return Err(e);
|
||||
}
|
||||
if let Err(e) = session_stdin.flush() {
|
||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
tracing::debug!("PowerShell session stopped");
|
||||
} else {
|
||||
tracing::debug!("PowerShell session already stopped");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_powershell(cmd: &str) -> Result<()> {
|
||||
if let Some(session_stdin) = SESSION_STDIN.lock().as_mut() {
|
||||
if let Err(e) = writeln!(session_stdin, "{cmd}") {
|
||||
tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
if let Err(e) = session_stdin.flush() {
|
||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(Error::new(
|
||||
ErrorKind::NotFound,
|
||||
"PowerShell session not started",
|
||||
))
|
||||
}
|
||||
|
||||
pub struct Komobar {
|
||||
pub hwnd: Option<isize>,
|
||||
pub monitor_index: Option<usize>,
|
||||
pub disabled: bool,
|
||||
pub config: KomobarConfig,
|
||||
pub render_config: Rc<RefCell<RenderConfig>>,
|
||||
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
pub monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub center_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
||||
@@ -82,6 +162,18 @@ pub struct Komobar {
|
||||
pub size_rect: komorebi_client::Rect,
|
||||
pub work_area_offset: komorebi_client::Rect,
|
||||
applied_theme_on_first_frame: bool,
|
||||
mouse_follows_focus: bool,
|
||||
input_config: InputConfig,
|
||||
}
|
||||
|
||||
struct InputConfig {
|
||||
accumulated_scroll_delta: Vec2,
|
||||
act_on_vertical_scroll: bool,
|
||||
act_on_horizontal_scroll: bool,
|
||||
vertical_scroll_threshold: f32,
|
||||
horizontal_scroll_threshold: f32,
|
||||
vertical_scroll_max_threshold: f32,
|
||||
horizontal_scroll_max_threshold: f32,
|
||||
}
|
||||
|
||||
pub fn apply_theme(
|
||||
@@ -250,7 +342,7 @@ impl Komobar {
|
||||
pub fn apply_config(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
previous_monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
||||
) {
|
||||
MAX_LABEL_WIDTH.store(
|
||||
self.config.max_label_width.unwrap_or(400.0) as i32,
|
||||
@@ -279,7 +371,7 @@ impl Komobar {
|
||||
self.config.icon_scale,
|
||||
));
|
||||
|
||||
let mut komorebi_notification_state = previous_notification_state;
|
||||
let mut monitor_info = previous_monitor_info;
|
||||
let mut komorebi_widgets = Vec::new();
|
||||
|
||||
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
|
||||
@@ -331,19 +423,18 @@ impl Komobar {
|
||||
komorebi_widgets
|
||||
.into_iter()
|
||||
.for_each(|(mut widget, idx, side)| {
|
||||
match komorebi_notification_state {
|
||||
match monitor_info {
|
||||
None => {
|
||||
komorebi_notification_state =
|
||||
Some(widget.komorebi_notification_state.clone());
|
||||
monitor_info = Some(widget.monitor_info.clone());
|
||||
}
|
||||
Some(ref previous) => {
|
||||
if widget.workspaces.is_some_and(|w| w.enable) {
|
||||
previous.borrow_mut().update_from_config(
|
||||
&widget.komorebi_notification_state.borrow(),
|
||||
);
|
||||
if widget.workspaces.is_some() {
|
||||
previous
|
||||
.borrow_mut()
|
||||
.update_from_self(&widget.monitor_info.borrow());
|
||||
}
|
||||
|
||||
widget.komorebi_notification_state = previous.clone();
|
||||
widget.monitor_info = previous.clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,15 +459,19 @@ impl Komobar {
|
||||
}
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
};
|
||||
let monitor_index = self.komorebi_notification_state.as_ref().and_then(|state| {
|
||||
state
|
||||
.borrow()
|
||||
.monitor_usr_idx_map
|
||||
.get(&usr_monitor_index)
|
||||
.copied()
|
||||
|
||||
let mapped_info = self.monitor_info.as_ref().map(|info| {
|
||||
let monitor = info.borrow();
|
||||
(
|
||||
monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
|
||||
monitor.mouse_follows_focus,
|
||||
)
|
||||
});
|
||||
|
||||
self.monitor_index = monitor_index;
|
||||
if let Some(info) = mapped_info {
|
||||
self.monitor_index = info.0;
|
||||
self.mouse_follows_focus = info.1;
|
||||
}
|
||||
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
|
||||
@@ -427,7 +522,7 @@ impl Komobar {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.komorebi_notification_state.is_some() && !self.disabled {
|
||||
} else if self.monitor_info.is_some() && !self.disabled {
|
||||
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
|
||||
self.disabled = true;
|
||||
} else {
|
||||
@@ -435,9 +530,39 @@ impl Komobar {
|
||||
self.disabled = true;
|
||||
}
|
||||
|
||||
if let Some(mouse) = &self.config.mouse {
|
||||
self.input_config.act_on_vertical_scroll =
|
||||
mouse.on_scroll_up.is_some() || mouse.on_scroll_down.is_some();
|
||||
self.input_config.act_on_horizontal_scroll =
|
||||
mouse.on_scroll_left.is_some() || mouse.on_scroll_right.is_some();
|
||||
self.input_config.vertical_scroll_threshold = mouse
|
||||
.vertical_scroll_threshold
|
||||
.unwrap_or(30.0)
|
||||
.clamp(10.0, 300.0);
|
||||
self.input_config.horizontal_scroll_threshold = mouse
|
||||
.horizontal_scroll_threshold
|
||||
.unwrap_or(30.0)
|
||||
.clamp(10.0, 300.0);
|
||||
// limit how many "ticks" can be accumulated
|
||||
self.input_config.vertical_scroll_max_threshold =
|
||||
self.input_config.vertical_scroll_threshold * 3.0;
|
||||
self.input_config.horizontal_scroll_max_threshold =
|
||||
self.input_config.horizontal_scroll_threshold * 3.0;
|
||||
|
||||
if mouse.has_command() {
|
||||
start_powershell().unwrap_or_else(|_| {
|
||||
tracing::error!("failed to start powershell session");
|
||||
});
|
||||
} else {
|
||||
stop_powershell().unwrap_or_else(|_| {
|
||||
tracing::error!("failed to stop powershell session");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("widget configuration options applied");
|
||||
|
||||
self.komorebi_notification_state = komorebi_notification_state;
|
||||
self.monitor_info = monitor_info;
|
||||
}
|
||||
|
||||
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
|
||||
@@ -506,8 +631,7 @@ impl Komobar {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
home
|
||||
@@ -521,26 +645,6 @@ impl Komobar {
|
||||
match komorebi_client::StaticConfig::read(&config) {
|
||||
Ok(config) => {
|
||||
if let Some(theme) = config.theme {
|
||||
let stack_accent = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name, stack_border, ..
|
||||
} => stack_border
|
||||
.unwrap_or(CatppuccinValue::Green)
|
||||
.color32(name.as_theme()),
|
||||
KomorebiTheme::Base16 {
|
||||
name, stack_border, ..
|
||||
} => stack_border
|
||||
.unwrap_or(Base16Value::Base0B)
|
||||
.color32(Base16Wrapper::Base16(name)),
|
||||
KomorebiTheme::Custom {
|
||||
ref colours,
|
||||
stack_border,
|
||||
..
|
||||
} => stack_border
|
||||
.unwrap_or(Base16Value::Base0B)
|
||||
.color32(Base16Wrapper::Custom(colours.clone())),
|
||||
};
|
||||
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
@@ -550,10 +654,6 @@ impl Komobar {
|
||||
bar_grouping,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
|
||||
if let Some(state) = &self.komorebi_notification_state {
|
||||
state.borrow_mut().stack_accent = Some(stack_accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -596,7 +696,7 @@ impl Komobar {
|
||||
disabled: false,
|
||||
config,
|
||||
render_config: Rc::new(RefCell::new(RenderConfig::new())),
|
||||
komorebi_notification_state: None,
|
||||
monitor_info: None,
|
||||
left_widgets: vec![],
|
||||
center_widgets: vec![],
|
||||
right_widgets: vec![],
|
||||
@@ -608,6 +708,16 @@ impl Komobar {
|
||||
size_rect: komorebi_client::Rect::default(),
|
||||
work_area_offset: komorebi_client::Rect::default(),
|
||||
applied_theme_on_first_frame: false,
|
||||
mouse_follows_focus: false,
|
||||
input_config: InputConfig {
|
||||
accumulated_scroll_delta: Vec2::new(0.0, 0.0),
|
||||
act_on_vertical_scroll: false,
|
||||
act_on_horizontal_scroll: false,
|
||||
vertical_scroll_threshold: 0.0,
|
||||
horizontal_scroll_threshold: 0.0,
|
||||
vertical_scroll_max_threshold: 0.0,
|
||||
horizontal_scroll_max_threshold: 0.0,
|
||||
},
|
||||
};
|
||||
|
||||
komobar.apply_config(&cc.egui_ctx, None);
|
||||
@@ -732,12 +842,12 @@ impl eframe::App for Komobar {
|
||||
|
||||
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
|
||||
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
}
|
||||
|
||||
if let Ok(updated_config) = self.rx_config.try_recv() {
|
||||
self.config = updated_config;
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
}
|
||||
|
||||
match self.rx_gui.try_recv() {
|
||||
@@ -862,24 +972,26 @@ impl eframe::App for Komobar {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
|
||||
komorebi_notification_state
|
||||
.borrow_mut()
|
||||
.handle_notification(
|
||||
ctx,
|
||||
self.monitor_index,
|
||||
notification,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme.clone(),
|
||||
self.render_config.clone(),
|
||||
);
|
||||
if let Some(monitor_info) = &self.monitor_info {
|
||||
monitor_info.borrow_mut().update(
|
||||
self.monitor_index,
|
||||
notification.state,
|
||||
self.render_config.borrow().show_all_icons,
|
||||
);
|
||||
handle_notification(
|
||||
ctx,
|
||||
notification.event,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme.clone(),
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if should_apply_config {
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
|
||||
// Reposition the Bar
|
||||
self.position_bar();
|
||||
@@ -961,6 +1073,111 @@ impl eframe::App for Komobar {
|
||||
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
||||
|
||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||
if let Some(mouse_config) = &self.config.mouse {
|
||||
let command = if ui
|
||||
.input(|i| i.pointer.button_double_clicked(PointerButton::Primary))
|
||||
{
|
||||
tracing::debug!("Input: primary button double clicked");
|
||||
&mouse_config.on_primary_double_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Secondary)) {
|
||||
tracing::debug!("Input: secondary button clicked");
|
||||
&mouse_config.on_secondary_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Middle)) {
|
||||
tracing::debug!("Input: middle button clicked");
|
||||
&mouse_config.on_middle_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Extra1)) {
|
||||
tracing::debug!("Input: extra1 button clicked");
|
||||
&mouse_config.on_extra1_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Extra2)) {
|
||||
tracing::debug!("Input: extra2 button clicked");
|
||||
&mouse_config.on_extra2_click
|
||||
} else if self.input_config.act_on_vertical_scroll
|
||||
|| self.input_config.act_on_horizontal_scroll
|
||||
{
|
||||
let scroll_delta = ui.input(|input| input.smooth_scroll_delta);
|
||||
|
||||
self.input_config.accumulated_scroll_delta += scroll_delta;
|
||||
|
||||
if scroll_delta.y != 0.0 && self.input_config.act_on_vertical_scroll {
|
||||
// Do not store more than the max threshold
|
||||
self.input_config.accumulated_scroll_delta.y =
|
||||
self.input_config.accumulated_scroll_delta.y.clamp(
|
||||
-self.input_config.vertical_scroll_max_threshold,
|
||||
self.input_config.vertical_scroll_max_threshold,
|
||||
);
|
||||
|
||||
// When the accumulated scroll passes the threshold, trigger a tick.
|
||||
if self.input_config.accumulated_scroll_delta.y.abs()
|
||||
>= self.input_config.vertical_scroll_threshold
|
||||
{
|
||||
let direction_command =
|
||||
if self.input_config.accumulated_scroll_delta.y > 0.0 {
|
||||
&mouse_config.on_scroll_up
|
||||
} else {
|
||||
&mouse_config.on_scroll_down
|
||||
};
|
||||
|
||||
// Remove one tick's worth of scroll from the accumulator, preserving any excess.
|
||||
self.input_config.accumulated_scroll_delta.y -=
|
||||
self.input_config.vertical_scroll_threshold
|
||||
* self.input_config.accumulated_scroll_delta.y.signum();
|
||||
|
||||
tracing::debug!(
|
||||
"Input: vertical scroll ticked. excess: {} | threshold: {}",
|
||||
self.input_config.accumulated_scroll_delta.y,
|
||||
self.input_config.vertical_scroll_threshold
|
||||
);
|
||||
|
||||
direction_command
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
} else if scroll_delta.x != 0.0 && self.input_config.act_on_horizontal_scroll {
|
||||
// Do not store more than the max threshold
|
||||
self.input_config.accumulated_scroll_delta.x =
|
||||
self.input_config.accumulated_scroll_delta.x.clamp(
|
||||
-self.input_config.horizontal_scroll_max_threshold,
|
||||
self.input_config.horizontal_scroll_max_threshold,
|
||||
);
|
||||
|
||||
// When the accumulated scroll passes the threshold, trigger a tick.
|
||||
if self.input_config.accumulated_scroll_delta.x.abs()
|
||||
>= self.input_config.horizontal_scroll_threshold
|
||||
{
|
||||
let direction_command =
|
||||
if self.input_config.accumulated_scroll_delta.x > 0.0 {
|
||||
&mouse_config.on_scroll_left
|
||||
} else {
|
||||
&mouse_config.on_scroll_right
|
||||
};
|
||||
|
||||
// Remove one tick's worth of scroll from the accumulator, preserving any excess.
|
||||
self.input_config.accumulated_scroll_delta.x -=
|
||||
self.input_config.horizontal_scroll_threshold
|
||||
* self.input_config.accumulated_scroll_delta.x.signum();
|
||||
|
||||
tracing::debug!(
|
||||
"Input: horizontal scroll ticked. excess: {} | threshold: {}",
|
||||
self.input_config.accumulated_scroll_delta.x,
|
||||
self.input_config.horizontal_scroll_threshold
|
||||
);
|
||||
|
||||
direction_command
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
|
||||
if let Some(command) = command {
|
||||
command.execute(self.mouse_follows_focus);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply grouping logic for the bar as a whole
|
||||
let area_frame = if let Some(frame) = &self.config.frame {
|
||||
Frame::NONE
|
||||
@@ -1100,3 +1317,64 @@ 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,3 +1,4 @@
|
||||
use crate::bar::exec_powershell;
|
||||
use crate::render::Grouping;
|
||||
use crate::widgets::widget::WidgetConfig;
|
||||
use crate::DEFAULT_PADDING;
|
||||
@@ -5,7 +6,9 @@ use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -13,7 +16,7 @@ use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.37`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.38`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar height (default: 50)
|
||||
pub height: Option<f32>,
|
||||
@@ -90,6 +93,8 @@ pub struct KomobarConfig {
|
||||
pub widget_spacing: Option<f32>,
|
||||
/// Visual grouping for widgets
|
||||
pub grouping: Option<Grouping>,
|
||||
/// Options for mouse interaction on the bar
|
||||
pub mouse: Option<MouseConfig>,
|
||||
/// Left side widgets (ordered left-to-right)
|
||||
pub left_widgets: Vec<WidgetConfig>,
|
||||
/// Center widgets (ordered left-to-right)
|
||||
@@ -325,6 +330,146 @@ pub fn get_individual_spacing(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum MouseMessage {
|
||||
/// Send a message to the komorebi client.
|
||||
/// By default, a batch of messages are sent in the following order:
|
||||
/// FocusMonitorAtCursor =>
|
||||
/// MouseFollowsFocus(false) =>
|
||||
/// {message} =>
|
||||
/// MouseFollowsFocus({original.value})
|
||||
///
|
||||
/// Example:
|
||||
/// ```json
|
||||
/// "on_extra2_click": {
|
||||
/// "message": {
|
||||
/// "type": "NewWorkspace"
|
||||
/// }
|
||||
/// },
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "on_middle_click": {
|
||||
/// "focus_monitor_at_cursor": false,
|
||||
/// "ignore_mouse_follows_focus": false,
|
||||
/// "message": {
|
||||
/// "type": "TogglePause"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "on_scroll_up": {
|
||||
/// "message": {
|
||||
/// "type": "CycleFocusWorkspace",
|
||||
/// "content": "Previous"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
Komorebi(KomorebiMouseMessage),
|
||||
/// Execute a custom command.
|
||||
/// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved.
|
||||
/// Example: `komorebic toggle-pause`
|
||||
Command(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct KomorebiMouseMessage {
|
||||
/// Send the FocusMonitorAtCursor message (default:true)
|
||||
pub focus_monitor_at_cursor: Option<bool>,
|
||||
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message (default:true)
|
||||
pub ignore_mouse_follows_focus: Option<bool>,
|
||||
/// The message to send to the komorebi client
|
||||
pub message: komorebi_client::SocketMessage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct MouseConfig {
|
||||
/// Command to send on primary/left double button click
|
||||
pub on_primary_double_click: Option<MouseMessage>,
|
||||
/// Command to send on secondary/right button click
|
||||
pub on_secondary_click: Option<MouseMessage>,
|
||||
/// Command to send on middle button click
|
||||
pub on_middle_click: Option<MouseMessage>,
|
||||
/// Command to send on extra1/back button click
|
||||
pub on_extra1_click: Option<MouseMessage>,
|
||||
/// Command to send on extra2/forward button click
|
||||
pub on_extra2_click: Option<MouseMessage>,
|
||||
|
||||
/// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
||||
pub vertical_scroll_threshold: Option<f32>,
|
||||
/// Command to send on scrolling up (every tick)
|
||||
pub on_scroll_up: Option<MouseMessage>,
|
||||
/// Command to send on scrolling down (every tick)
|
||||
pub on_scroll_down: Option<MouseMessage>,
|
||||
|
||||
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
||||
pub horizontal_scroll_threshold: Option<f32>,
|
||||
/// Command to send on scrolling left (every tick)
|
||||
pub on_scroll_left: Option<MouseMessage>,
|
||||
/// Command to send on scrolling right (every tick)
|
||||
pub on_scroll_right: Option<MouseMessage>,
|
||||
}
|
||||
|
||||
impl MouseConfig {
|
||||
pub fn has_command(&self) -> bool {
|
||||
[
|
||||
&self.on_primary_double_click,
|
||||
&self.on_secondary_click,
|
||||
&self.on_middle_click,
|
||||
&self.on_extra1_click,
|
||||
&self.on_extra2_click,
|
||||
&self.on_scroll_up,
|
||||
&self.on_scroll_down,
|
||||
&self.on_scroll_left,
|
||||
&self.on_scroll_right,
|
||||
]
|
||||
.iter()
|
||||
.any(|opt| matches!(opt, Some(MouseMessage::Command(_))))
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseMessage {
|
||||
pub fn execute(&self, mouse_follows_focus: bool) {
|
||||
match self {
|
||||
MouseMessage::Komorebi(config) => {
|
||||
let mut messages = Vec::new();
|
||||
|
||||
if config.focus_monitor_at_cursor.unwrap_or(true) {
|
||||
messages.push(SocketMessage::FocusMonitorAtCursor);
|
||||
}
|
||||
|
||||
if config.ignore_mouse_follows_focus.unwrap_or(true) {
|
||||
messages.push(SocketMessage::MouseFollowsFocus(false));
|
||||
messages.push(config.message.clone());
|
||||
messages.push(SocketMessage::MouseFollowsFocus(mouse_follows_focus));
|
||||
} else {
|
||||
messages.push(config.message.clone());
|
||||
}
|
||||
|
||||
tracing::debug!("Sending messages: {messages:?}");
|
||||
|
||||
if komorebi_client::send_batch(messages.into_iter()).is_err() {
|
||||
tracing::error!("could not send commands");
|
||||
}
|
||||
}
|
||||
MouseMessage::Command(cmd) => {
|
||||
tracing::debug!("Executing command: {}", cmd);
|
||||
|
||||
let cmd_no_env = cmd.replace_env();
|
||||
|
||||
if exec_powershell(cmd_no_env.to_str().expect("Invalid command")).is_err() {
|
||||
tracing::error!("Failed to execute '{}'", cmd);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
|
||||
@@ -15,12 +15,10 @@ use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::replace_env_in_path;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::SubscribeOptions;
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
@@ -28,8 +26,6 @@ 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;
|
||||
@@ -53,9 +49,6 @@ pub static DEFAULT_PADDING: f32 = 10.0;
|
||||
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
struct Opts {
|
||||
@@ -166,8 +159,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
home
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::komorebi::img_to_texture;
|
||||
use super::ImageIcon;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
@@ -17,14 +17,13 @@ use eframe::egui::Stroke;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use image::DynamicImage;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::PathExt;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tracing;
|
||||
@@ -119,42 +118,32 @@ impl BarWidget for Applications {
|
||||
|
||||
impl From<&ApplicationsConfig> for Applications {
|
||||
fn from(applications_config: &ApplicationsConfig) -> Self {
|
||||
// Allow immediate launch by initializing last_launch in the past.
|
||||
let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL;
|
||||
let mut applications_config = applications_config.clone();
|
||||
let items = applications_config
|
||||
.items
|
||||
.iter_mut()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, app_config)| {
|
||||
app_config.command = app_config
|
||||
.command
|
||||
.replace_env()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
if let Some(icon) = &mut app_config.icon {
|
||||
*icon = icon.replace_env().to_string_lossy().to_string();
|
||||
}
|
||||
.map(|(index, config)| {
|
||||
let command = UserCommand::new(&config.command);
|
||||
|
||||
App {
|
||||
enable: app_config.enable.unwrap_or(applications_config.enable),
|
||||
name: app_config
|
||||
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(|| app_config.name.clone()),
|
||||
icon: Icon::try_from(app_config),
|
||||
command: app_config.command.clone(),
|
||||
display: app_config
|
||||
.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: app_config
|
||||
show_command_on_hover: config
|
||||
.show_command_on_hover
|
||||
.or(applications_config.show_command_on_hover)
|
||||
.unwrap_or(false),
|
||||
last_launch,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -177,13 +166,11 @@ pub struct App {
|
||||
/// Icon to display for this application, if available.
|
||||
pub icon: Option<Icon>,
|
||||
/// Command to execute when the application is launched.
|
||||
pub command: String,
|
||||
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,
|
||||
/// Last time this application was launched (used for cooldown control).
|
||||
pub last_launch: Instant,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -205,17 +192,15 @@ impl App {
|
||||
}
|
||||
|
||||
// Add hover text with command information
|
||||
let response = ui.response();
|
||||
if self.show_command_on_hover {
|
||||
ui.response()
|
||||
.on_hover_text(format!("Launch: {}", self.command));
|
||||
} else {
|
||||
ui.response();
|
||||
response.on_hover_text(format!("Launch: {}", self.command.as_ref()));
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
// Launch the application when clicked
|
||||
self.launch_if_ready();
|
||||
self.command.launch_if_ready();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,84 +220,75 @@ impl App {
|
||||
fn draw_name(&self, ui: &mut Ui) {
|
||||
ui.add(Label::new(&self.name).selectable(false));
|
||||
}
|
||||
|
||||
/// 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();
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds decoded image data to be used as an icon in the UI.
|
||||
/// 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(RgbaImage),
|
||||
Image(ImageIcon),
|
||||
/// Text-based icon, e.g. from a font like Nerd Fonts.
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
/// Attempts to create an `Icon` from the given `AppConfig`.
|
||||
/// Loads the image from a specified icon path or extracts it from the application's
|
||||
/// executable if the command points to a valid executable file.
|
||||
/// 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(config: &AppConfig) -> Option<Self> {
|
||||
if let Some(icon) = config.icon.as_deref().map(str::trim) {
|
||||
if !icon.is_empty() {
|
||||
let path = Path::new(&icon);
|
||||
if path.is_file() {
|
||||
match image::open(path).as_ref().map(DynamicImage::to_rgba8) {
|
||||
Ok(image) => return Some(Icon::Image(image)),
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to load icon from {}, error: {}", icon, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Some(Icon::Text(icon.to_owned()));
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let binary = PathBuf::from(config.command.split(".exe").next()?);
|
||||
let path = if binary.is_file() {
|
||||
Some(binary)
|
||||
} else {
|
||||
which(binary).ok()
|
||||
};
|
||||
|
||||
match path {
|
||||
Some(path) => windows_icons::get_icon_by_path(&path.to_string_lossy())
|
||||
.or_else(|| windows_icons_fallback::get_icon_by_path(&path.to_string_lossy()))
|
||||
.map(Icon::Image),
|
||||
None => None,
|
||||
}
|
||||
Some(Icon::Image(image_icon))
|
||||
}
|
||||
|
||||
/// Renders the icon in the given `Ui` context with the specified size.
|
||||
/// 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::Image(image_icon) => {
|
||||
Frame::NONE
|
||||
.inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Image::from(&img_to_texture(ctx, image))
|
||||
Image::from_texture(&image_icon.texture(ctx))
|
||||
.maintain_aspect_ratio(true)
|
||||
.fit_to_exact_size(Vec2::splat(icon_config.size)),
|
||||
);
|
||||
@@ -354,3 +330,77 @@ pub struct IconConfig {
|
||||
/// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ impl BarWidget for Battery {
|
||||
.args(["/C", "start", "ms-settings:batterysaver"])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
eprintln!("{error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,8 +76,8 @@ impl Cpu {
|
||||
|
||||
CpuOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {used}%"),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{used}%"),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
@@ -124,7 +124,7 @@ impl BarWidget for Cpu {
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
eprintln!("{error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,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())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,8 +41,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
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 +52,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}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,6 +187,12 @@ impl KomorebiLayout {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
// TODO: @CtByte can you think of a nice icon to draw here?
|
||||
komorebi_client::DefaultLayout::Scrolling => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
},
|
||||
KomorebiLayout::Monocle => {}
|
||||
KomorebiLayout::Floating => {
|
||||
|
||||
@@ -79,9 +79,9 @@ impl Memory {
|
||||
MemoryOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", usage)
|
||||
format!("RAM: {usage}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{usage}%"),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
@@ -128,7 +128,7 @@ impl BarWidget for Memory {
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
eprintln!("{error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
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;
|
||||
@@ -12,3 +23,151 @@ pub mod storage;
|
||||
pub mod time;
|
||||
pub mod update;
|
||||
pub mod widget;
|
||||
|
||||
/// Global cache for icon images and their associated GPU textures.
|
||||
pub static ICONS_CACHE: IconsCache = IconsCache::new();
|
||||
|
||||
/// In-memory cache for icon images and their associated GPU textures.
|
||||
///
|
||||
/// Stores raw [`ColorImage`]s and [`TextureHandle`]s keyed by [`ImageIconId`].
|
||||
/// Texture entries are context-dependent and automatically invalidated when the [`Context`] changes.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct IconsCache {
|
||||
textures: LazyLock<RwLock<(Option<Context>, HashMap<ImageIconId, TextureHandle>)>>,
|
||||
images: LazyLock<RwLock<HashMap<ImageIconId, Arc<ColorImage>>>>,
|
||||
}
|
||||
|
||||
impl IconsCache {
|
||||
/// Creates a new empty IconsCache instance.
|
||||
#[inline]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
textures: LazyLock::new(|| RwLock::new((None, HashMap::new()))),
|
||||
images: LazyLock::new(|| RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves or creates a texture handle for the given icon ID and image.
|
||||
///
|
||||
/// If a texture for the given ID already exists for the current [`Context`], it is reused.
|
||||
/// Otherwise, a new texture is created, inserted into the cache, and returned.
|
||||
/// The cache is reset if the [`Context`] has changed.
|
||||
#[inline]
|
||||
pub fn texture(&self, ctx: &Context, id: &ImageIconId, img: &Arc<ColorImage>) -> TextureHandle {
|
||||
if let Some(texture) = self.get_texture(ctx, id) {
|
||||
return texture;
|
||||
}
|
||||
let texture_handle = ctx.load_texture("icon", img.clone(), TextureOptions::default());
|
||||
self.insert_texture(ctx, id.clone(), texture_handle.clone());
|
||||
texture_handle
|
||||
}
|
||||
|
||||
/// Returns the cached texture for the given icon ID if it exists and matches the current [`Context`].
|
||||
pub fn get_texture(&self, ctx: &Context, id: &ImageIconId) -> Option<TextureHandle> {
|
||||
let textures_lock = self.textures.read().unwrap();
|
||||
if textures_lock.0.as_ref() == Some(ctx) {
|
||||
return textures_lock.1.get(id).cloned();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Inserts a texture handle, resetting the cache if the [`Context`] has changed.
|
||||
pub fn insert_texture(&self, ctx: &Context, id: ImageIconId, texture: TextureHandle) {
|
||||
let mut textures_lock = self.textures.write().unwrap();
|
||||
|
||||
if textures_lock.0.as_ref() != Some(ctx) {
|
||||
textures_lock.0 = Some(ctx.clone());
|
||||
textures_lock.1.clear();
|
||||
}
|
||||
|
||||
textures_lock.1.insert(id, texture);
|
||||
}
|
||||
|
||||
/// Returns the cached image for the given icon ID, if available.
|
||||
pub fn get_image(&self, id: &ImageIconId) -> Option<Arc<ColorImage>> {
|
||||
self.images.read().unwrap().get(id).cloned()
|
||||
}
|
||||
|
||||
/// Caches a raw [`ColorImage`] associated with the given icon ID.
|
||||
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
|
||||
self.images.write().unwrap().insert(id, image);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
|
||||
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||
let pixels = rgba_image.as_flat_samples();
|
||||
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
|
||||
}
|
||||
|
||||
/// Represents an image-based icon with a unique ID and pixel data.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImageIcon {
|
||||
/// Unique identifier for the image icon, used for texture caching.
|
||||
pub id: ImageIconId,
|
||||
/// Shared pixel data of the icon in `ColorImage` format.
|
||||
pub image: Arc<ColorImage>,
|
||||
}
|
||||
|
||||
impl ImageIcon {
|
||||
/// Creates a new [`ImageIcon`] from the given ID and image data.
|
||||
#[inline]
|
||||
pub fn new(id: ImageIconId, image: Arc<ColorImage>) -> Self {
|
||||
Self { id, image }
|
||||
}
|
||||
|
||||
/// Loads an [`ImageIcon`] from [`ICONS_CACHE`] or calls `loader` if not cached.
|
||||
/// The loaded image is converted to a [`ColorImage`], cached, and returned.
|
||||
#[inline]
|
||||
pub fn try_load<F, I>(id: impl Into<ImageIconId>, loader: F) -> Option<Self>
|
||||
where
|
||||
F: FnOnce() -> Option<I>,
|
||||
I: Into<RgbaImage>,
|
||||
{
|
||||
let id = id.into();
|
||||
let image = ICONS_CACHE.get_image(&id).or_else(|| {
|
||||
let img = loader()?;
|
||||
let img = Arc::new(rgba_to_color_image(&img.into()));
|
||||
ICONS_CACHE.insert_image(id.clone(), img.clone());
|
||||
Some(img)
|
||||
})?;
|
||||
|
||||
Some(ImageIcon::new(id, image))
|
||||
}
|
||||
|
||||
/// Returns a texture handle for the icon, using the given [`Context`].
|
||||
///
|
||||
/// If the texture is already cached in [`ICONS_CACHE`], it is reused.
|
||||
/// Otherwise, a new texture is created from the [`ColorImage`] and cached.
|
||||
#[inline]
|
||||
pub fn texture(&self, ctx: &Context) -> TextureHandle {
|
||||
ICONS_CACHE.texture(ctx, &self.id, &self.image)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unique identifier for an image-based icon.
|
||||
///
|
||||
/// Used to distinguish cached images and textures by either a file path
|
||||
/// or a Windows window handle.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum ImageIconId {
|
||||
/// Identifier based on a file system path.
|
||||
Path(Arc<Path>),
|
||||
/// Windows HWND handle.
|
||||
Hwnd(isize),
|
||||
}
|
||||
|
||||
impl From<&Path> for ImageIconId {
|
||||
#[inline]
|
||||
fn from(value: &Path) -> Self {
|
||||
Self::Path(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<isize> for ImageIconId {
|
||||
#[inline]
|
||||
fn from(value: isize) -> Self {
|
||||
Self::Hwnd(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +314,7 @@ impl Network {
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
||||
eprintln!("{}", error);
|
||||
eprintln!("{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,6 +535,6 @@ enum DataUnit {
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ pub struct StorageConfig {
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Show disks that are read only. (default: false)
|
||||
pub show_read_only_disks: Option<bool>,
|
||||
/// Show removable disks. (default: true)
|
||||
pub show_removable_disks: Option<bool>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
/// Hide when the current percentage is under this value [[1-100]]
|
||||
@@ -38,6 +42,8 @@ impl From<StorageConfig> for Storage {
|
||||
disks: Disks::new_with_refreshed_list(),
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
|
||||
show_removable_disks: value.show_removable_disks.unwrap_or(true),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now(),
|
||||
@@ -55,6 +61,8 @@ pub struct Storage {
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
show_read_only_disks: bool,
|
||||
show_removable_disks: bool,
|
||||
auto_select_over: Option<u8>,
|
||||
auto_hide_under: Option<u8>,
|
||||
last_updated: Instant,
|
||||
@@ -71,6 +79,12 @@ impl Storage {
|
||||
let mut disks = vec![];
|
||||
|
||||
for disk in &self.disks {
|
||||
if disk.is_read_only() && !self.show_read_only_disks {
|
||||
continue;
|
||||
}
|
||||
if disk.is_removable() && !self.show_removable_disks {
|
||||
continue;
|
||||
}
|
||||
let mount = disk.mount_point();
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
@@ -87,7 +101,7 @@ impl Storage {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), percentage)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
|
||||
},
|
||||
selected,
|
||||
})
|
||||
@@ -151,7 +165,7 @@ impl BarWidget for Storage {
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
eprintln!("{error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -148,7 +148,7 @@ impl BarWidget for Update {
|
||||
)])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
eprintln!("{error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi = { path = "../komorebi" }
|
||||
komorebi = { path = "../komorebi", default-features = false }
|
||||
|
||||
uds_windows = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["komorebi/schemars"]
|
||||
schemars = ["komorebi/default"]
|
||||
|
||||
@@ -77,6 +77,7 @@ pub use komorebi::WorkspaceConfig;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
@@ -94,12 +95,15 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
|
||||
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()>
|
||||
where
|
||||
Q: Borrow<SocketMessage>,
|
||||
{
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
|
||||
if let Ok(m_str) = serde_json::to_string(&m) {
|
||||
if let Ok(m_str) = serde_json::to_string(m.borrow()) {
|
||||
s.push_str(&m_str);
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
|
||||
@@ -4,8 +4,8 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "29df24ff2dd715655b0366bd2a598837c699a8e9" }
|
||||
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "29df24ff2dd715655b0366bd2a598837c699a8e9" }
|
||||
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,6 +1,5 @@
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use std::path::PathBuf;
|
||||
use whkd_core::HotkeyBinding;
|
||||
use whkd_core::Whkdrc;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -58,21 +57,18 @@ impl eframe::App for Quicklook {
|
||||
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();
|
||||
ui.end_row();
|
||||
|
||||
for binding in &whkdrc.bindings {
|
||||
if is_komorebic_binding(binding) {
|
||||
let keys = binding.keys.join(" + ");
|
||||
if self.filter.is_empty()
|
||||
|| binding.command.contains(&self.filter)
|
||||
{
|
||||
ui.label(keys);
|
||||
ui.label(&binding.command);
|
||||
ui.end_row();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +96,3 @@ fn main() {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn is_komorebic_binding(binding: &HotkeyBinding) -> bool {
|
||||
binding.command.starts_with("komorebic")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
description = "A tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
@@ -17,7 +17,6 @@ crossbeam-channel = { workspace = true }
|
||||
crossbeam-utils = { workspace = true }
|
||||
ctrlc = { version = "3", features = ["termination"] }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
getset = "0.1"
|
||||
hotwatch = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
@@ -25,7 +24,7 @@ miow = "0.6"
|
||||
nanoid = "0.4"
|
||||
net2 = "0.2"
|
||||
os_info = "3.10"
|
||||
parking_lot = "0.12"
|
||||
parking_lot = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
powershell_script = "1.0"
|
||||
regex = "1"
|
||||
|
||||
@@ -17,5 +17,5 @@ pub enum AnimationPrefix {
|
||||
}
|
||||
|
||||
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
|
||||
format!("{}:{}", prefix, key)
|
||||
format!("{prefix}:{key}")
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ impl Border {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if !rect.is_same_size_as(&old_rect) {
|
||||
if !rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect) {
|
||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::core::BorderStyle;
|
||||
use crate::core::WindowKind;
|
||||
use crate::ring::Ring;
|
||||
use crate::windows_api;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
@@ -212,6 +213,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
[focused_workspace_idx]
|
||||
.layer();
|
||||
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
let forced_update = matches!(notification, Notification::ForceUpdate);
|
||||
|
||||
drop(state);
|
||||
|
||||
@@ -234,6 +237,17 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
|
||||
if ws.layer() == &WorkspaceLayer::Floating {
|
||||
for window in ws.floating_windows() {
|
||||
let mut window_kind = WindowKind::Unfocused;
|
||||
|
||||
if foreground_window == window.hwnd {
|
||||
window_kind = WindowKind::Floating;
|
||||
}
|
||||
|
||||
window.set_accent(window_kind_colour(window_kind))?;
|
||||
}
|
||||
}
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
@@ -241,7 +255,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let window_kind = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
if c.locked() {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
@@ -342,7 +356,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
};
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
tracing::debug!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
@@ -448,14 +462,40 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
windows_borders.insert(focused_window_hwnd, id);
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
// Remove all borders on this monitor except monocle
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| border_hwnd != b.hwnd,
|
||||
)?;
|
||||
|
||||
if ws.layer() == &WorkspaceLayer::Floating {
|
||||
handle_floating_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
ws,
|
||||
monitor_idx,
|
||||
foreground_window,
|
||||
layer_changed,
|
||||
forced_update,
|
||||
)?;
|
||||
|
||||
// Remove all borders on this monitor except monocle and floating borders
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| {
|
||||
border_hwnd != b.hwnd
|
||||
&& !ws
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.any(|w| w.hwnd == b.tracking_hwnd)
|
||||
},
|
||||
)?;
|
||||
} else {
|
||||
// Remove all borders on this monitor except monocle
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| border_hwnd != b.hwnd,
|
||||
)?;
|
||||
}
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
@@ -523,7 +563,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
|| focused_window_hwnd != foreground_window
|
||||
{
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
if c.locked() {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
@@ -569,9 +609,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
};
|
||||
border.window_rect = rect;
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
let forced_update = matches!(notification, Notification::ForceUpdate);
|
||||
|
||||
let should_invalidate = new_border
|
||||
|| (last_focus_state != new_focus_state)
|
||||
|| layer_changed
|
||||
@@ -591,65 +628,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
windows_borders.insert(focused_window_hwnd, id);
|
||||
}
|
||||
|
||||
{
|
||||
for window in ws.floating_windows() {
|
||||
let mut new_border = false;
|
||||
let id = window.hwnd.to_string();
|
||||
let border = match borders.entry(id.clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(
|
||||
&window.hwnd.to_string(),
|
||||
window.hwnd,
|
||||
monitor_idx,
|
||||
) {
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let last_focus_state = border.window_kind;
|
||||
|
||||
let new_focus_state = if foreground_window == window.hwnd {
|
||||
WindowKind::Floating
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
};
|
||||
|
||||
border.window_kind = new_focus_state;
|
||||
|
||||
// Update the border's monitor idx in case it changed
|
||||
border.monitor_idx = Some(monitor_idx);
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
border.window_rect = rect;
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
let forced_update =
|
||||
matches!(notification, Notification::ForceUpdate);
|
||||
|
||||
let should_invalidate = new_border
|
||||
|| (last_focus_state != new_focus_state)
|
||||
|| layer_changed
|
||||
|| forced_update;
|
||||
|
||||
if should_invalidate {
|
||||
if forced_update && !new_border {
|
||||
// Update the border brushes if there was a forced update
|
||||
// notification and this is not a new border (new border's
|
||||
// already have their brushes updated on creation)
|
||||
border.update_brushes()?;
|
||||
}
|
||||
border.set_position(&rect, window.hwnd)?;
|
||||
border.invalidate();
|
||||
}
|
||||
|
||||
windows_borders.insert(window.hwnd, id);
|
||||
}
|
||||
}
|
||||
handle_floating_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
ws,
|
||||
monitor_idx,
|
||||
foreground_window,
|
||||
layer_changed,
|
||||
forced_update,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -665,6 +652,68 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_floating_borders(
|
||||
borders: &mut HashMap<String, Box<Border>>,
|
||||
windows_borders: &mut HashMap<isize, String>,
|
||||
ws: &Workspace,
|
||||
monitor_idx: usize,
|
||||
foreground_window: isize,
|
||||
layer_changed: bool,
|
||||
forced_update: bool,
|
||||
) -> color_eyre::Result<()> {
|
||||
for window in ws.floating_windows() {
|
||||
let mut new_border = false;
|
||||
let id = window.hwnd.to_string();
|
||||
let border = match borders.entry(id.clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) =
|
||||
Border::create(&window.hwnd.to_string(), window.hwnd, monitor_idx)
|
||||
{
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let last_focus_state = border.window_kind;
|
||||
|
||||
let new_focus_state = if foreground_window == window.hwnd {
|
||||
WindowKind::Floating
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
};
|
||||
|
||||
border.window_kind = new_focus_state;
|
||||
|
||||
// Update the border's monitor idx in case it changed
|
||||
border.monitor_idx = Some(monitor_idx);
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
border.window_rect = rect;
|
||||
|
||||
let should_invalidate =
|
||||
new_border || (last_focus_state != new_focus_state) || layer_changed || forced_update;
|
||||
|
||||
if should_invalidate {
|
||||
if forced_update && !new_border {
|
||||
// Update the border brushes if there was a forced update
|
||||
// notification and this is not a new border (new border's
|
||||
// already have their brushes updated on creation)
|
||||
border.update_brushes()?;
|
||||
}
|
||||
border.set_position(&rect, window.hwnd)?;
|
||||
border.invalidate();
|
||||
}
|
||||
|
||||
windows_borders.insert(window.hwnd, id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes all borders from monitor with index `monitor_idx` filtered by
|
||||
/// `condition`. This condition is a function that will take a reference to
|
||||
/// the container id and the border and returns a bool, if true that border
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use getset::CopyGetters;
|
||||
use getset::Getters;
|
||||
use getset::Setters;
|
||||
use nanoid::nanoid;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
use crate::Lockable;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, CopyGetters, Setters)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Container {
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
#[serde(default)]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
locked: bool,
|
||||
windows: Ring<Window>,
|
||||
}
|
||||
|
||||
@@ -22,11 +28,23 @@ 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() {
|
||||
@@ -144,6 +162,7 @@ impl Container {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json;
|
||||
|
||||
#[test]
|
||||
fn test_contains_window() {
|
||||
@@ -250,4 +269,40 @@ mod tests {
|
||||
// Should return None since window 4 doesn't exist
|
||||
assert_eq!(container.idx_for_window(4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_with_missing_locked_field_defaults_to_false() {
|
||||
let json = r#"{
|
||||
"id": "test-1",
|
||||
"windows": { "elements": [], "focused": 0 }
|
||||
}"#;
|
||||
let container: Container = serde_json::from_str(json).expect("Should deserialize");
|
||||
|
||||
assert!(!container.locked());
|
||||
assert_eq!(container.id(), "test-1");
|
||||
assert!(container.windows().is_empty());
|
||||
|
||||
let json = r#"{
|
||||
"id": "test-2",
|
||||
"windows": { "elements": [ { "hwnd": 5 }, { "hwnd": 9 } ], "focused": 1 }
|
||||
}"#;
|
||||
let container: Container = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(container.id(), "test-2");
|
||||
assert!(!container.locked());
|
||||
assert_eq!(container.windows(), &[Window::from(5), Window::from(9)]);
|
||||
assert_eq!(container.focused_window_idx(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_and_deserializes() {
|
||||
let mut container = Container::default();
|
||||
container.set_locked(true);
|
||||
|
||||
let serialized = serde_json::to_string(&container).expect("Should serialize");
|
||||
let deserialized: Container =
|
||||
serde_json::from_str(&serialized).expect("Should deserialize");
|
||||
|
||||
assert!(deserialized.locked());
|
||||
assert_eq!(deserialized.id(), container.id());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Rect;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
|
||||
pub trait Arrangement {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
@@ -21,6 +23,9 @@ pub trait Arrangement {
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
focused_idx: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
latest_layout: &[Rect],
|
||||
) -> Vec<Rect>;
|
||||
}
|
||||
|
||||
@@ -33,9 +38,110 @@ impl Arrangement for DefaultLayout {
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
focused_idx: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
latest_layout: &[Rect],
|
||||
) -> Vec<Rect> {
|
||||
let len = usize::from(len);
|
||||
let mut dimensions = match self {
|
||||
Self::Scrolling => {
|
||||
let column_count = layout_options
|
||||
.and_then(|o| o.scrolling.map(|s| s.columns))
|
||||
.unwrap_or(3);
|
||||
|
||||
let column_width = area.right / column_count as i32;
|
||||
let mut layouts = Vec::with_capacity(len);
|
||||
|
||||
match len {
|
||||
// treat < 3 windows the same as the columns layout
|
||||
len if len < 3 => {
|
||||
layouts = columns(area, len);
|
||||
|
||||
let adjustment = calculate_columns_adjustment(resize_dimensions);
|
||||
layouts.iter_mut().zip(adjustment.iter()).for_each(
|
||||
|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
},
|
||||
);
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
columns_reverse(&mut layouts);
|
||||
}
|
||||
}
|
||||
}
|
||||
// treat >= column_count as scrolling
|
||||
len => {
|
||||
let visible_columns = area.right / column_width;
|
||||
let first_visible: isize = if focused_idx == 0 {
|
||||
// if focused idx is 0, we are at the beginning of the scrolling strip
|
||||
0
|
||||
} else {
|
||||
let previous_first_visible = if latest_layout.is_empty() {
|
||||
0
|
||||
} else {
|
||||
// previous first_visible based on the left position of the first visible window
|
||||
let left_edge = area.left;
|
||||
latest_layout
|
||||
.iter()
|
||||
.position(|rect| rect.left >= left_edge)
|
||||
.unwrap_or(0) as isize
|
||||
};
|
||||
|
||||
let focused_idx = focused_idx as isize;
|
||||
|
||||
if focused_idx < previous_first_visible {
|
||||
// focused window is off the left edge, we need to scroll left
|
||||
focused_idx
|
||||
} else if focused_idx
|
||||
>= previous_first_visible + visible_columns as isize
|
||||
{
|
||||
// focused window is off the right edge, we need to scroll right
|
||||
// and make sure it's the last visible window
|
||||
(focused_idx + 1 - visible_columns as isize).max(0)
|
||||
} else {
|
||||
// focused window is already visible, we don't need to scroll
|
||||
previous_first_visible
|
||||
}
|
||||
.min(
|
||||
(len as isize)
|
||||
.saturating_sub(visible_columns as isize)
|
||||
.max(0),
|
||||
)
|
||||
};
|
||||
|
||||
for i in 0..len {
|
||||
let position = (i as isize) - first_visible;
|
||||
let left = area.left + (position as i32 * column_width);
|
||||
|
||||
layouts.push(Rect {
|
||||
left,
|
||||
top: area.top,
|
||||
right: column_width,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
|
||||
layouts.iter_mut().zip(adjustment.iter()).for_each(
|
||||
|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::BSP => recursive_fibonacci(
|
||||
0,
|
||||
len,
|
||||
@@ -487,6 +593,9 @@ impl Arrangement for CustomLayout {
|
||||
container_padding: Option<i32>,
|
||||
_layout_flip: Option<Axis>,
|
||||
_resize_dimensions: &[Option<Rect>],
|
||||
_focused_idx: usize,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
_latest_layout: &[Rect],
|
||||
) -> Vec<Rect> {
|
||||
let mut dimensions = vec![];
|
||||
let container_count = len.get();
|
||||
@@ -541,7 +650,7 @@ impl Arrangement for CustomLayout {
|
||||
};
|
||||
|
||||
match column {
|
||||
Column::Primary(Option::Some(_)) => {
|
||||
Column::Primary(Some(_)) => {
|
||||
let main_column_area = if idx == 0 {
|
||||
Self::main_column_area(area, primary_right, None)
|
||||
} else {
|
||||
@@ -1115,6 +1224,37 @@ fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rec
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_scrolling_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
|
||||
if len <= 1 {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (i, rect) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
let is_leftmost = i == 0;
|
||||
let is_rightmost = i == len - 1;
|
||||
|
||||
resize_left(&mut result[i], rect.left);
|
||||
resize_right(&mut result[i], rect.right);
|
||||
resize_top(&mut result[i], rect.top);
|
||||
resize_bottom(&mut result[i], rect.bottom);
|
||||
|
||||
if !is_leftmost && rect.left != 0 {
|
||||
resize_right(&mut result[i - 1], rect.left);
|
||||
}
|
||||
|
||||
if !is_rightmost && rect.right != 0 {
|
||||
resize_left(&mut result[i + 1], rect.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn resize_left(rect: &mut Rect, resize: i32) {
|
||||
rect.left += resize / 2;
|
||||
rect.right += -resize / 2;
|
||||
|
||||
@@ -21,9 +21,24 @@ pub enum DefaultLayout {
|
||||
UltrawideVerticalStack,
|
||||
Grid,
|
||||
RightMainVerticalStack,
|
||||
Scrolling,
|
||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct LayoutOptions {
|
||||
/// Options related to the Scrolling layout
|
||||
pub scrolling: Option<ScrollingLayoutOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ScrollingLayoutOptions {
|
||||
/// Desired number of visible columns (default: 3)
|
||||
pub columns: usize,
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
@@ -31,6 +46,7 @@ impl DefaultLayout {
|
||||
n if n > 1 => 1,
|
||||
_ => 0,
|
||||
},
|
||||
Self::Scrolling => 0,
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
@@ -53,6 +69,7 @@ impl DefaultLayout {
|
||||
_ => len.saturating_sub(1),
|
||||
},
|
||||
DefaultLayout::RightMainVerticalStack => 0,
|
||||
DefaultLayout::Scrolling => len.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +92,7 @@ impl DefaultLayout {
|
||||
| Self::RightMainVerticalStack
|
||||
| Self::HorizontalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::Scrolling
|
||||
) {
|
||||
return None;
|
||||
};
|
||||
@@ -169,13 +187,15 @@ impl DefaultLayout {
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
Self::RightMainVerticalStack => Self::Scrolling,
|
||||
Self::Scrolling => Self::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn cycle_previous(self) -> Self {
|
||||
match self {
|
||||
Self::Scrolling => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||
|
||||
@@ -102,6 +102,7 @@ impl Direction for DefaultLayout {
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx > 2,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Down => match self {
|
||||
Self::BSP => idx != count - 1 && idx % 2 != 0,
|
||||
@@ -111,6 +112,7 @@ impl Direction for DefaultLayout {
|
||||
Self::HorizontalStack => idx == 0,
|
||||
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
Self::BSP => idx != 0,
|
||||
@@ -120,6 +122,7 @@ impl Direction for DefaultLayout {
|
||||
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx != 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => idx != 0,
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
Self::BSP => idx % 2 == 0 && idx != count - 1,
|
||||
@@ -133,6 +136,7 @@ impl Direction for DefaultLayout {
|
||||
_ => idx < 2,
|
||||
},
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => idx != count - 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -158,6 +162,7 @@ impl Direction for DefaultLayout {
|
||||
| Self::RightMainVerticalStack => idx - 1,
|
||||
Self::HorizontalStack => 0,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +181,7 @@ impl Direction for DefaultLayout {
|
||||
Self::Columns => unreachable!(),
|
||||
Self::HorizontalStack => 1,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +209,7 @@ impl Direction for DefaultLayout {
|
||||
_ => 0,
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => idx - 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +230,7 @@ impl Direction for DefaultLayout {
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => idx + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -108,6 +109,7 @@ pub enum SocketMessage {
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(DefaultLayout),
|
||||
CycleLayout(CycleDirection),
|
||||
ScrollingLayoutColumns(NonZeroUsize),
|
||||
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
FlipLayout(Axis),
|
||||
ToggleWorkspaceWindowContainerBehaviour,
|
||||
@@ -200,6 +202,7 @@ pub enum SocketMessage {
|
||||
StackbarFontFamily(Option<String>),
|
||||
WorkAreaOffset(Rect),
|
||||
MonitorWorkAreaOffset(usize, Rect),
|
||||
WorkspaceWorkAreaOffset(usize, usize, Rect),
|
||||
ToggleWindowBasedWorkAreaOffset,
|
||||
ResizeDelta(i32),
|
||||
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
@@ -341,6 +344,7 @@ pub enum StateQuery {
|
||||
FocusedWindowIndex,
|
||||
FocusedWorkspaceName,
|
||||
FocusedWorkspaceLayout,
|
||||
FocusedContainerKind,
|
||||
Version,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ use serde::Serialize;
|
||||
|
||||
/// Path extension trait
|
||||
pub trait PathExt {
|
||||
/// Resolve environment variables components in a path.
|
||||
/// Resolve environment variable components in a path.
|
||||
///
|
||||
/// Resolves the follwing formats:
|
||||
/// Resolves the following formats:
|
||||
/// - CMD: `%variable%`
|
||||
/// - PowerShell: `$Env:variable`
|
||||
/// - Bash: `$variable`.
|
||||
|
||||
@@ -41,6 +41,10 @@ impl Rect {
|
||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||
self.right == rhs.right && self.bottom == rhs.bottom
|
||||
}
|
||||
|
||||
pub fn has_same_position_as(&self, rhs: &Self) -> bool {
|
||||
self.left == rhs.left && self.top == rhs.top
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
|
||||
@@ -8,7 +8,7 @@ pub mod ring;
|
||||
pub mod container;
|
||||
pub mod core;
|
||||
pub mod focus_manager;
|
||||
pub mod locked_deque;
|
||||
pub mod lockable_sequence;
|
||||
pub mod monitor;
|
||||
pub mod monitor_reconciliator;
|
||||
pub mod process_command;
|
||||
@@ -200,8 +200,7 @@ lazy_static! {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
|
||||
@@ -255,6 +254,14 @@ pub static WINDOW_HANDLING_BEHAVIOUR: AtomicCell<WindowHandlingBehaviour> =
|
||||
|
||||
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);
|
||||
@@ -299,7 +306,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
current
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
@@ -316,7 +323,7 @@ pub enum VirtualDesktopNotification {
|
||||
LeftAssociatedVirtualDesktop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
|
||||
357
komorebi/src/lockable_sequence.rs
Normal file
357
komorebi/src/lockable_sequence.rs
Normal file
@@ -0,0 +1,357 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::Lockable;
|
||||
|
||||
/// A sequence supporting insertion, removal, and swapping of elements while preserving the absolute
|
||||
/// positions of locked items.
|
||||
pub trait LockableSequence<T: Lockable> {
|
||||
/// Inserts a value at `idx`, keeping locked elements at their absolute positions.
|
||||
fn insert_respecting_locks(&mut self, idx: usize, value: T) -> usize;
|
||||
/// Removes the element at `idx`, keeping locked elements at their absolute positions.
|
||||
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T>;
|
||||
/// Swaps the elements at indices `i` and `j`, keeping locked elements at their absolute positions.
|
||||
fn swap_respecting_locks(&mut self, i: usize, j: usize);
|
||||
}
|
||||
|
||||
impl<T: Lockable> LockableSequence<T> for VecDeque<T> {
|
||||
/// Insert `value` at logical index `idx`, with trying to keep locked elements
|
||||
/// (`is_locked()`) anchored at their original positions.
|
||||
///
|
||||
/// Returns the final index of the inserted element.
|
||||
fn insert_respecting_locks(&mut self, mut idx: usize, value: T) -> usize {
|
||||
// 1. Bounds check: if index is out of range, simply append.
|
||||
if idx >= self.len() {
|
||||
self.push_back(value);
|
||||
return self.len() - 1; // last index
|
||||
}
|
||||
|
||||
// 2. Normal VecDeque insertion
|
||||
self.insert(idx, value);
|
||||
|
||||
// 3. Walk left-to-right once, swapping any misplaced locked element. After
|
||||
// the VecDeque::insert all items after `idx` have moved right by one. For every locked
|
||||
// element that is now to the right of an unlocked one, swap it back left exactly once.
|
||||
for index in (idx + 1)..self.len() {
|
||||
if self[index].locked() && !self[index - 1].locked() {
|
||||
self.swap(index - 1, index);
|
||||
|
||||
// If the element we just inserted participated in the swap,
|
||||
// update `idx` so we can return its final location.
|
||||
if idx == index - 1 {
|
||||
idx = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
/// Remove element at `idx`, with trying to keep locked elements
|
||||
/// (`is_locked()`) anchored at their original positions.
|
||||
///
|
||||
/// Returns the removed element, or `None` if `idx` is out of bounds.
|
||||
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T> {
|
||||
// 1. Bounds check: if index is out of range, do nothing.
|
||||
if idx >= self.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Remove the element at the requested index.
|
||||
// All elements after idx are now shifted left by 1.
|
||||
let removed = self.remove(idx)?;
|
||||
|
||||
// 3. If less than 2 elements remain, nothing to shift.
|
||||
if self.len() < 2 {
|
||||
return Some(removed);
|
||||
}
|
||||
|
||||
// 4. Iterate from the element just after the removed spot up to the second-to-last
|
||||
// element, right-to-left. This loop "fixes" locked elements that were shifted left
|
||||
// off their anchored positions: If a locked element now has an unlocked element
|
||||
// to its right, swap them back to restore locked order.
|
||||
for index in (idx..self.len() - 1).rev() {
|
||||
// If current is locked and the next one is not locked, swap them.
|
||||
if self[index].locked() && !self[index + 1].locked() {
|
||||
self.swap(index, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Return the removed value.
|
||||
Some(removed)
|
||||
}
|
||||
|
||||
/// Swaps the elements at indices `i` and `j`, along with their `locked` status, ensuring
|
||||
/// the lock state remains associated with the position rather than the element itself.
|
||||
fn swap_respecting_locks(&mut self, i: usize, j: usize) {
|
||||
self.swap(i, j);
|
||||
let locked_i = self[i].locked();
|
||||
let locked_j = self[j].locked();
|
||||
self[i].set_locked(locked_j);
|
||||
self[j].set_locked(locked_i);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct TestItem {
|
||||
val: i32,
|
||||
locked: bool,
|
||||
}
|
||||
|
||||
impl Lockable for TestItem {
|
||||
fn locked(&self) -> bool {
|
||||
self.locked
|
||||
}
|
||||
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self {
|
||||
self.locked = locked;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn vals(v: &VecDeque<TestItem>) -> Vec<i32> {
|
||||
v.iter().map(|x| x.val).collect()
|
||||
}
|
||||
|
||||
fn test_deque(items: &[(i32, bool)]) -> VecDeque<TestItem> {
|
||||
items
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|(val, locked)| TestItem { val, locked })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_respecting_locks() {
|
||||
// Test case 1: Basic insertion with locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
// Insert at index 0, should shift elements while keeping index 2 locked
|
||||
ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
|
||||
assert_eq!(vals(&ring), vec![99, 0, 2, 1, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 2: Insert at a locked index (should insert after locked)
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
// Try to insert at locked index 2, should insert at index 3 instead
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
2,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 3);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 99, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 3: Multiple locked indices
|
||||
{
|
||||
// Lock index 1 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
|
||||
// Insert at index 0, should maintain locked indices
|
||||
ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Elements '1' and '3' remain at indices 1 and 3
|
||||
assert_eq!(vals(&ring), vec![99, 1, 0, 3, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 4: Insert at end
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
5,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
|
||||
}
|
||||
|
||||
// Test case 5: Empty ring
|
||||
{
|
||||
let mut ring = test_deque(&[]);
|
||||
// Insert into empty deque
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 0);
|
||||
assert_eq!(vals(&ring), vec![99]);
|
||||
}
|
||||
|
||||
// Test case 6: All indices locked
|
||||
{
|
||||
// Lock all indices
|
||||
let mut ring = test_deque(&[(0, true), (1, true), (2, true), (3, true), (4, true)]);
|
||||
// Try to insert at index 2, should insert at the end
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
2,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
|
||||
}
|
||||
|
||||
// Test case 7: Consecutive locked indices
|
||||
{
|
||||
// Lock index 2 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, true), (4, false)]);
|
||||
// Insert at index 1, should maintain consecutive locked indices
|
||||
ring.insert_respecting_locks(
|
||||
1,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Elements '2' and '3' remain at indices 2 and 3
|
||||
assert_eq!(vals(&ring), vec![0, 99, 2, 3, 1, 4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_respecting_locks() {
|
||||
// Test case 1: Remove a non-locked index before a locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(0);
|
||||
assert_eq!(removed.map(|x| x.val), Some(0));
|
||||
// Elements '2' remain at index 2
|
||||
assert_eq!(vals(&ring), vec![1, 3, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 2: Remove a locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(2);
|
||||
assert_eq!(removed.map(|x| x.val), Some(2));
|
||||
// Elements should stay at the same places
|
||||
assert_eq!(vals(&ring), vec![0, 1, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 3: Remove an index after a locked index
|
||||
{
|
||||
// Lock index 1
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(3);
|
||||
assert_eq!(removed.map(|x| x.val), Some(3));
|
||||
// Elements should stay at the same places
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 4: Multiple locked indices
|
||||
{
|
||||
// Lock index 1 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(0);
|
||||
assert_eq!(removed.map(|x| x.val), Some(0));
|
||||
// Elements '1' and '3' remain at indices '1' and '3'
|
||||
assert_eq!(vals(&ring), vec![2, 1, 4, 3]);
|
||||
}
|
||||
|
||||
// Test case 5: Remove the last element
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(4);
|
||||
assert_eq!(removed.map(|x| x.val), Some(4));
|
||||
// Index 2 should still be at the same place
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
|
||||
}
|
||||
|
||||
// Test case 6: Invalid index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(10);
|
||||
assert_eq!(removed, None);
|
||||
// Deque unchanged
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 7: Remove enough elements to make a locked index invalid
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true)]);
|
||||
ring.remove_respecting_locks(0);
|
||||
// Index 2 should now be '1'
|
||||
assert_eq!(vals(&ring), vec![1, 2]);
|
||||
}
|
||||
|
||||
// Test case 8: Removing an element before multiple locked indices
|
||||
{
|
||||
// Lock index 2 and 4
|
||||
let mut ring = test_deque(&[
|
||||
(0, false),
|
||||
(1, false),
|
||||
(2, true),
|
||||
(3, false),
|
||||
(4, true),
|
||||
(5, false),
|
||||
]);
|
||||
let removed = ring.remove_respecting_locks(1);
|
||||
assert_eq!(removed.map(|x| x.val), Some(1));
|
||||
// Both indices should still be at the same place
|
||||
assert_eq!(vals(&ring), vec![0, 3, 2, 5, 4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swap_respecting_locks_various_cases() {
|
||||
// Swap unlocked and locked
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false)]);
|
||||
ring.swap_respecting_locks(0, 1);
|
||||
assert_eq!(vals(&ring), vec![1, 0, 2, 3]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
ring.swap_respecting_locks(0, 1);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
|
||||
// Both locked
|
||||
let mut ring = test_deque(&[(0, true), (1, false), (2, true)]);
|
||||
ring.swap_respecting_locks(0, 2);
|
||||
assert_eq!(vals(&ring), vec![2, 1, 0]);
|
||||
assert!(ring[0].locked);
|
||||
assert!(!ring[1].locked);
|
||||
assert!(ring[2].locked);
|
||||
|
||||
// Both unlocked
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false)]);
|
||||
ring.swap_respecting_locks(0, 2);
|
||||
assert_eq!(vals(&ring), vec![2, 1, 0]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
assert!(!ring[2].locked);
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct LockedDeque<'a, T> {
|
||||
deque: &'a mut VecDeque<T>,
|
||||
locked_indices: &'a mut BTreeSet<usize>,
|
||||
}
|
||||
|
||||
impl<'a, T: PartialEq> LockedDeque<'a, T> {
|
||||
pub fn new(deque: &'a mut VecDeque<T>, locked_indices: &'a mut BTreeSet<usize>) -> Self {
|
||||
Self {
|
||||
deque,
|
||||
locked_indices,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, index: usize, value: T) -> usize {
|
||||
insert_respecting_locks(self.deque, self.locked_indices, index, value)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, index: usize) -> Option<T> {
|
||||
remove_respecting_locks(self.deque, self.locked_indices, index)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_respecting_locks<T>(
|
||||
deque: &mut VecDeque<T>,
|
||||
locked_idx: &mut BTreeSet<usize>,
|
||||
idx: usize,
|
||||
value: T,
|
||||
) -> usize {
|
||||
if idx == deque.len() {
|
||||
deque.push_back(value);
|
||||
return idx;
|
||||
}
|
||||
|
||||
let mut new_deque = VecDeque::with_capacity(deque.len() + 1);
|
||||
let mut temp_locked_deque = VecDeque::new();
|
||||
let mut j = 0;
|
||||
let mut corrected_idx = idx;
|
||||
|
||||
for (i, el) in deque.drain(..).enumerate() {
|
||||
if i == idx {
|
||||
corrected_idx = j;
|
||||
}
|
||||
if locked_idx.contains(&i) {
|
||||
temp_locked_deque.push_back(el);
|
||||
} else {
|
||||
new_deque.push_back(el);
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
|
||||
new_deque.insert(corrected_idx, value);
|
||||
|
||||
for (locked_el, locked_idx) in temp_locked_deque.into_iter().zip(locked_idx.iter()) {
|
||||
new_deque.insert(*locked_idx, locked_el);
|
||||
if *locked_idx <= corrected_idx {
|
||||
corrected_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
*deque = new_deque;
|
||||
|
||||
corrected_idx
|
||||
}
|
||||
|
||||
pub fn remove_respecting_locks<T>(
|
||||
deque: &mut VecDeque<T>,
|
||||
locked_idx: &mut BTreeSet<usize>,
|
||||
idx: usize,
|
||||
) -> Option<T> {
|
||||
if idx >= deque.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let final_size = deque.len() - 1;
|
||||
|
||||
let mut new_deque = VecDeque::with_capacity(final_size);
|
||||
let mut temp_locked_deque = VecDeque::new();
|
||||
let mut removed = None;
|
||||
let mut removed_locked_idx = None;
|
||||
|
||||
for (i, el) in deque.drain(..).enumerate() {
|
||||
if i == idx {
|
||||
removed = Some(el);
|
||||
removed_locked_idx = locked_idx.contains(&i).then_some(i);
|
||||
} else if locked_idx.contains(&i) {
|
||||
temp_locked_deque.push_back(el);
|
||||
} else {
|
||||
new_deque.push_back(el);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(i) = removed_locked_idx {
|
||||
let mut above = locked_idx.split_off(&i);
|
||||
above.pop_first();
|
||||
locked_idx.extend(above.into_iter().map(|i| i - 1));
|
||||
}
|
||||
|
||||
while locked_idx.last().is_some_and(|i| *i >= final_size) {
|
||||
locked_idx.pop_last();
|
||||
}
|
||||
|
||||
let extra_invalid_idx = (new_deque.len()
|
||||
..(new_deque.len() + temp_locked_deque.len() - locked_idx.len()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (locked_el, locked_idx) in temp_locked_deque
|
||||
.into_iter()
|
||||
.zip(locked_idx.iter().chain(extra_invalid_idx.iter()))
|
||||
{
|
||||
new_deque.insert(*locked_idx, locked_el);
|
||||
}
|
||||
|
||||
*deque = new_deque;
|
||||
|
||||
removed
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[test]
|
||||
fn test_insert_respecting_locks() {
|
||||
// Test case 1: Basic insertion with locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
// Insert at index 0, should shift elements while keeping index 2 locked
|
||||
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||
assert_eq!(deque, VecDeque::from(vec![99, 0, 2, 1, 3, 4]));
|
||||
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
|
||||
}
|
||||
|
||||
// Test case 2: Insert at a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
// Try to insert at locked index 2, should insert at index 3 instead
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
|
||||
assert_eq!(actual_index, 3);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 99, 3, 4]));
|
||||
}
|
||||
|
||||
// Test case 3: Multiple locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(1); // Lock index 1
|
||||
locked.insert(3); // Lock index 3
|
||||
|
||||
// Insert at index 0, should maintain locked indices
|
||||
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||
assert_eq!(deque, VecDeque::from(vec![99, 1, 0, 3, 2, 4]));
|
||||
// Elements '1' and '3' remain at indices 1 and 3
|
||||
}
|
||||
|
||||
// Test case 4: Insert at end
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
// Insert at end of deque
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 5, 99);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
|
||||
}
|
||||
|
||||
// Test case 5: Empty deque
|
||||
{
|
||||
let mut deque = VecDeque::new();
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// Insert into empty deque
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||
assert_eq!(actual_index, 0);
|
||||
assert_eq!(deque, VecDeque::from(vec![99]));
|
||||
}
|
||||
|
||||
// Test case 6: All indices locked
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
for i in 0..5 {
|
||||
locked.insert(i); // Lock all indices
|
||||
}
|
||||
|
||||
// Try to insert at index 2, should insert at the end
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
|
||||
}
|
||||
|
||||
// Test case 7: Consecutive locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
locked.insert(3); // Lock index 3
|
||||
|
||||
// Insert at index 1, should maintain consecutive locked indices
|
||||
insert_respecting_locks(&mut deque, &mut locked, 1, 99);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 99, 2, 3, 1, 4]));
|
||||
// Elements '2' and '3' remain at indices 2 and 3
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_respecting_locks() {
|
||||
// Test case 1: Remove a non-locked index before a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||
assert_eq!(removed, Some(0));
|
||||
assert_eq!(deque, VecDeque::from(vec![1, 3, 2, 4]));
|
||||
assert!(locked.contains(&2)); // Index 2 should still be locked
|
||||
}
|
||||
|
||||
// Test case 2: Remove a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 2);
|
||||
assert_eq!(removed, Some(2));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 3, 4]));
|
||||
assert!(!locked.contains(&2)); // Index 2 should be unlocked
|
||||
}
|
||||
|
||||
// Test case 3: Remove an index after a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(1); // Lock index 1
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 3);
|
||||
assert_eq!(removed, Some(3));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 4]));
|
||||
assert!(locked.contains(&1)); // Index 1 should still be locked
|
||||
}
|
||||
|
||||
// Test case 4: Multiple locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(1); // Lock index 1
|
||||
locked.insert(3); // Lock index 3
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||
assert_eq!(removed, Some(0));
|
||||
assert_eq!(deque, VecDeque::from(vec![2, 1, 4, 3]));
|
||||
assert!(locked.contains(&1) && locked.contains(&3)); // Both indices should still be locked
|
||||
}
|
||||
|
||||
// Test case 5: Remove the last element
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 4);
|
||||
assert_eq!(removed, Some(4));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3]));
|
||||
assert!(locked.contains(&2)); // Index 2 should still be locked
|
||||
}
|
||||
|
||||
// Test case 6: Invalid index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 10);
|
||||
assert_eq!(removed, None);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4])); // Deque unchanged
|
||||
assert!(locked.contains(&2)); // Lock unchanged
|
||||
}
|
||||
|
||||
// Test case 7: Remove enough elements to make a locked index invalid
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||
assert_eq!(deque, VecDeque::from(vec![1, 2]));
|
||||
assert!(!locked.contains(&2)); // Index 2 should now be invalid
|
||||
}
|
||||
|
||||
// Test case 8: Removing an element before multiple locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4, 5]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
locked.insert(4); // Lock index 4
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 1);
|
||||
assert_eq!(removed, Some(1));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 3, 2, 5, 4]));
|
||||
assert!(locked.contains(&2) && locked.contains(&4)); // Both indices should still be locked
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,7 +500,7 @@ impl Monitor {
|
||||
if workspaces.get(idx).is_none() {
|
||||
workspaces.resize(idx + 1, Workspace::default());
|
||||
}
|
||||
|
||||
self.set_last_focused_workspace(Some(self.workspaces.focused_idx()));
|
||||
self.workspaces.focus(idx);
|
||||
}
|
||||
|
||||
@@ -631,6 +631,25 @@ mod tests {
|
||||
assert_eq!(m.workspaces().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_nonexistent_workspace() {
|
||||
let mut m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Try to remove a workspace that doesn't exist
|
||||
let removed_workspace = m.remove_workspace_by_idx(1);
|
||||
|
||||
// Should return None since there is no workspace at index 1
|
||||
assert!(removed_workspace.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focus_workspace() {
|
||||
let mut m = Monitor::new(
|
||||
@@ -738,6 +757,46 @@ mod tests {
|
||||
assert_eq!(m.focused_workspace().unwrap().containers().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_container_to_nonexistent_workspace() {
|
||||
let mut m = Monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
{
|
||||
// Create workspace 1 and add 3 containers
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
for _ in 0..3 {
|
||||
let container = Container::default();
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
|
||||
// Should have 3 containers in workspace 1
|
||||
assert_eq!(m.focused_workspace().unwrap().containers().len(), 3);
|
||||
}
|
||||
|
||||
// Should only have 1 workspace
|
||||
assert_eq!(m.workspaces().len(), 1);
|
||||
|
||||
// Try to move a container to a workspace that doesn't exist
|
||||
m.move_container_to_workspace(8, true, None).unwrap();
|
||||
|
||||
// Should have 9 workspaces now
|
||||
assert_eq!(m.workspaces().len(), 9);
|
||||
|
||||
// Should be focused on workspace 8
|
||||
assert_eq!(m.focused_workspace_idx(), 8);
|
||||
|
||||
// Should have 1 container in workspace 8
|
||||
assert_eq!(m.focused_workspace().unwrap().containers().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_workspace_count_workspace_contains_two_workspaces() {
|
||||
let mut m = Monitor::new(
|
||||
|
||||
@@ -801,7 +801,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}");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -829,7 +829,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}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,7 +849,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,
|
||||
@@ -960,7 +960,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}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ use crate::core::StateQuery;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowKind;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use crate::default_layout::ScrollingLayoutOptions;
|
||||
use crate::monitor::MonitorInformation;
|
||||
use crate::notify_subscribers;
|
||||
use crate::stackbar_manager;
|
||||
@@ -207,25 +209,6 @@ impl WindowManager {
|
||||
// We don't have From implemented for &mut WindowManager
|
||||
let initial_state = State::from(self.as_ref());
|
||||
|
||||
match message {
|
||||
SocketMessage::CycleFocusEmptyWorkspace(_)
|
||||
| SocketMessage::CycleFocusWorkspace(_)
|
||||
| SocketMessage::FocusWorkspaceNumber(_) => {
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
let idx = monitor.focused_workspace_idx();
|
||||
monitor.set_last_focused_workspace(Option::from(idx));
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusMonitorWorkspaceNumber(target_monitor_idx, _) => {
|
||||
let idx = self.focused_workspace_idx_for_monitor_idx(target_monitor_idx)?;
|
||||
if let Some(monitor) = self.monitors_mut().get_mut(target_monitor_idx) {
|
||||
monitor.set_last_focused_workspace(Option::from(idx));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut force_update_borders = false;
|
||||
match message {
|
||||
SocketMessage::Promote => self.promote_container_to_front()?,
|
||||
@@ -347,7 +330,7 @@ impl WindowManager {
|
||||
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
|
||||
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
|
||||
SocketMessage::StackAll => self.stack_all()?,
|
||||
SocketMessage::UnstackAll => self.unstack_all()?,
|
||||
SocketMessage::UnstackAll => self.unstack_all(true)?,
|
||||
SocketMessage::CycleStack(direction) => {
|
||||
self.cycle_container_window_in_direction(direction)?;
|
||||
}
|
||||
@@ -391,7 +374,9 @@ impl WindowManager {
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_eyre("no workspace at the given index")?;
|
||||
|
||||
workspace.locked_containers.insert(container_idx);
|
||||
if let Some(container) = workspace.containers_mut().get_mut(container_idx) {
|
||||
container.set_locked(true);
|
||||
}
|
||||
}
|
||||
SocketMessage::UnlockMonitorWorkspaceContainer(
|
||||
monitor_idx,
|
||||
@@ -408,7 +393,9 @@ impl WindowManager {
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_eyre("no workspace at the given index")?;
|
||||
|
||||
workspace.locked_containers.remove(&container_idx);
|
||||
if let Some(container) = workspace.containers_mut().get_mut(container_idx) {
|
||||
container.set_locked(false);
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleLock => self.toggle_lock()?,
|
||||
SocketMessage::ToggleFloat => self.toggle_float(false)?,
|
||||
@@ -933,6 +920,27 @@ impl WindowManager {
|
||||
self.retile_all(true)?
|
||||
}
|
||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
||||
SocketMessage::ScrollingLayoutColumns(count) => {
|
||||
let focused_workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let options = match focused_workspace.layout_options() {
|
||||
Some(mut opts) => {
|
||||
if let Some(scrolling) = &mut opts.scrolling {
|
||||
scrolling.columns = count.into();
|
||||
}
|
||||
|
||||
opts
|
||||
}
|
||||
None => LayoutOptions {
|
||||
scrolling: Some(ScrollingLayoutOptions {
|
||||
columns: count.into(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
focused_workspace.set_layout_options(Some(options));
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
||||
SocketMessage::ChangeLayoutCustom(ref path) => {
|
||||
@@ -1281,6 +1289,7 @@ impl WindowManager {
|
||||
if i == focused_idx {
|
||||
to_focus = Some(*window);
|
||||
} else {
|
||||
window.restore();
|
||||
window.raise()?;
|
||||
}
|
||||
}
|
||||
@@ -1288,6 +1297,7 @@ impl WindowManager {
|
||||
if let Some(focused_window) = &to_focus {
|
||||
// The focused window should be the last one raised to make sure it is
|
||||
// on top
|
||||
focused_window.restore();
|
||||
focused_window.raise()?;
|
||||
}
|
||||
|
||||
@@ -1297,8 +1307,8 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(container) = workspace.monocle_container() {
|
||||
if let Some(window) = container.focused_window() {
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
window.lower()?;
|
||||
}
|
||||
}
|
||||
@@ -1306,30 +1316,41 @@ impl WindowManager {
|
||||
WorkspaceLayer::Floating => {
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
for (i, container) in workspace.containers_mut().iter_mut().enumerate() {
|
||||
if let Some(window) = container.focused_window() {
|
||||
if i == focused_container_idx {
|
||||
to_focus = Some(*window);
|
||||
}
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
to_focus = Some(*window);
|
||||
window.raise()?;
|
||||
}
|
||||
}
|
||||
for window in workspace.floating_windows() {
|
||||
window.hide();
|
||||
}
|
||||
} else {
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
for (i, container) in workspace.containers_mut().iter_mut().enumerate()
|
||||
{
|
||||
if let Some(window) = container.focused_window() {
|
||||
if i == focused_container_idx {
|
||||
to_focus = Some(*window);
|
||||
}
|
||||
window.raise()?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut window_idx_pairs = workspace
|
||||
.floating_windows_mut()
|
||||
.make_contiguous()
|
||||
.iter()
|
||||
.collect::<Vec<_>>();
|
||||
let mut window_idx_pairs = workspace
|
||||
.floating_windows_mut()
|
||||
.make_contiguous()
|
||||
.iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort by window area
|
||||
window_idx_pairs.sort_by_key(|w| {
|
||||
let rect = WindowsApi::window_rect(w.hwnd).unwrap_or_default();
|
||||
rect.right * rect.bottom
|
||||
});
|
||||
// Sort by window area
|
||||
window_idx_pairs.sort_by_key(|w| {
|
||||
let rect = WindowsApi::window_rect(w.hwnd).unwrap_or_default();
|
||||
rect.right * rect.bottom
|
||||
});
|
||||
|
||||
for window in window_idx_pairs {
|
||||
window.lower()?;
|
||||
for window in window_idx_pairs {
|
||||
window.lower()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1462,6 +1483,18 @@ impl WindowManager {
|
||||
},
|
||||
)
|
||||
}
|
||||
StateQuery::FocusedContainerKind => {
|
||||
match self.focused_workspace()?.focused_container() {
|
||||
None => "None".to_string(),
|
||||
Some(container) => {
|
||||
if container.windows().len() > 1 {
|
||||
"Stack".to_string()
|
||||
} else {
|
||||
"Single".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
reply.write_all(response.as_bytes())?;
|
||||
@@ -1726,7 +1759,7 @@ Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue
|
||||
{
|
||||
for config_file_path in &mut *display_bar_configurations {
|
||||
let script = r#"Start-Process "komorebi-bar" '"--config" "CONFIGFILE"' -WindowStyle hidden"#
|
||||
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
|
||||
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
|
||||
|
||||
match powershell_script::run(&script) {
|
||||
Ok(_) => {
|
||||
@@ -1853,6 +1886,14 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
|
||||
self.retile_all(false)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::WorkspaceWorkAreaOffset(monitor_idx, workspace_idx, rect) => {
|
||||
if let Some(monitor) = self.monitors_mut().get_mut(monitor_idx) {
|
||||
if let Some(workspace) = monitor.workspaces_mut().get_mut(workspace_idx) {
|
||||
workspace.set_work_area_offset(Option::from(rect));
|
||||
self.retile_all(false)?
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleWindowBasedWorkAreaOffset => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.set_apply_window_based_work_area_offset(
|
||||
|
||||
@@ -25,6 +25,8 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Layout;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
@@ -301,7 +303,11 @@ impl WindowManager {
|
||||
// don't want to trigger the full workspace updates when there are no managed
|
||||
// containers - this makes floating windows on empty workspaces go into very
|
||||
// annoying focus change loops which prevents users from interacting with them
|
||||
if !self.focused_workspace()?.containers().is_empty() {
|
||||
if !matches!(
|
||||
self.focused_workspace()?.layout(),
|
||||
Layout::Default(DefaultLayout::Scrolling)
|
||||
) && !self.focused_workspace()?.containers().is_empty()
|
||||
{
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
}
|
||||
|
||||
@@ -328,6 +334,14 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
|
||||
if matches!(
|
||||
self.focused_workspace()?.layout(),
|
||||
Layout::Default(DefaultLayout::Scrolling)
|
||||
) && !self.focused_workspace()?.containers().is_empty()
|
||||
{
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
}
|
||||
}
|
||||
Some(idx) => {
|
||||
if let Some(_window) = workspace.floating_windows().get(idx) {
|
||||
@@ -339,10 +353,11 @@ impl WindowManager {
|
||||
WindowManagerEvent::Show(_, window)
|
||||
| WindowManagerEvent::Manage(window)
|
||||
| WindowManagerEvent::Uncloak(_, window) => {
|
||||
if matches!(event, WindowManagerEvent::Uncloak(_, _)) && self.uncloak_to_ignore >= 1
|
||||
if matches!(event, WindowManagerEvent::Uncloak(_, _))
|
||||
&& self.uncloack_to_ignore >= 1
|
||||
{
|
||||
tracing::info!("ignoring uncloak after monocle move by mouse across monitors");
|
||||
self.uncloak_to_ignore = self.uncloak_to_ignore.saturating_sub(1);
|
||||
self.uncloack_to_ignore = self.uncloack_to_ignore.saturating_sub(1);
|
||||
} else {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
@@ -401,7 +416,6 @@ impl WindowManager {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let workspace_contains_window = workspace.contains_window(window.hwnd);
|
||||
let monocle_container = workspace.monocle_container().clone();
|
||||
let mut workspace_layer = *workspace.layer();
|
||||
|
||||
if !workspace_contains_window && needs_reconciliation.is_none() {
|
||||
let floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
@@ -445,7 +459,6 @@ impl WindowManager {
|
||||
placement.should_center() && workspace.tile;
|
||||
workspace.floating_windows_mut().push_back(window);
|
||||
workspace.set_layer(WorkspaceLayer::Floating);
|
||||
workspace_layer = *workspace.layer();
|
||||
if center_spawned_floats {
|
||||
let mut floating_window = window;
|
||||
floating_window.center(
|
||||
@@ -459,7 +472,6 @@ impl WindowManager {
|
||||
WindowContainerBehaviour::Create => {
|
||||
workspace.new_container_for_window(window);
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
workspace_layer = *workspace.layer();
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
@@ -470,7 +482,6 @@ impl WindowManager {
|
||||
})?
|
||||
.add_window(window);
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
workspace_layer = *workspace.layer();
|
||||
self.update_focused_workspace(true, false)?;
|
||||
stackbar_manager::send_notification();
|
||||
}
|
||||
@@ -503,9 +514,10 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
if !monocle_window_event
|
||||
let workspace = self.focused_workspace()?;
|
||||
if !(monocle_window_event
|
||||
|| workspace.layer() != &WorkspaceLayer::Tiling)
|
||||
&& monocle_container.is_some()
|
||||
&& matches!(workspace_layer, WorkspaceLayer::Tiling)
|
||||
{
|
||||
window.hide();
|
||||
}
|
||||
|
||||
@@ -43,10 +43,6 @@ impl<T> Ring<T> {
|
||||
pub fn focused_mut(&mut self) -> Option<&mut T> {
|
||||
self.elements.get_mut(self.focused)
|
||||
}
|
||||
|
||||
pub fn swap(&mut self, i: usize, j: usize) {
|
||||
self.elements.swap(i, j);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_ring_elements {
|
||||
|
||||
@@ -28,8 +28,8 @@ pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947);
|
||||
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
|
||||
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
|
||||
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
|
||||
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
|
||||
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::OnStack);
|
||||
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Title);
|
||||
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::Never);
|
||||
|
||||
pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
|
||||
@@ -58,15 +58,19 @@ use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
|
||||
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::IDC_ARROW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
@@ -312,6 +316,16 @@ impl Stackbar {
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match msg {
|
||||
WM_SETCURSOR => match LoadCursorW(None, IDC_ARROW) {
|
||||
Ok(cursor) => {
|
||||
SetCursor(Some(cursor));
|
||||
LRESULT(0)
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
LRESULT(1)
|
||||
}
|
||||
},
|
||||
WM_LBUTTONDOWN => {
|
||||
let stackbars_containers = STACKBARS_CONTAINERS.lock();
|
||||
if let Some(container) = stackbars_containers.get(&(hwnd.0 as isize)) {
|
||||
|
||||
@@ -35,6 +35,7 @@ use crate::core::StackbarMode;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowManagementBehaviour;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use crate::monitor;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::monitor_reconciliator;
|
||||
@@ -191,6 +192,9 @@ pub struct WorkspaceConfig {
|
||||
/// Layout (default: BSP)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout: Option<DefaultLayout>,
|
||||
/// Layout-specific options (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_options: Option<LayoutOptions>,
|
||||
/// END OF LIFE FEATURE: Custom Layout (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde_as(as = "Option<ResolvedPathBuf>")]
|
||||
@@ -214,6 +218,9 @@ pub struct WorkspaceConfig {
|
||||
/// Permanent workspace application rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub workspace_rules: Option<Vec<MatchingRule>>,
|
||||
/// Workspace specific work area offset (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub work_area_offset: Option<Rect>,
|
||||
/// Apply this monitor's window-based work area offset (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub apply_window_based_work_area_offset: Option<bool>,
|
||||
@@ -226,6 +233,9 @@ pub struct WorkspaceConfig {
|
||||
/// Enable or disable float override, which makes it so every new window opens in floating mode (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_override: Option<bool>,
|
||||
/// Enable or disable tiling for the workspace (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tile: Option<bool>,
|
||||
/// Specify an axis on which to flip the selected layout (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_flip: Option<Axis>,
|
||||
@@ -274,6 +284,8 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
}
|
||||
});
|
||||
|
||||
let tile = if *value.tile() { None } else { Some(false) };
|
||||
|
||||
Self {
|
||||
name: value
|
||||
.name()
|
||||
@@ -286,6 +298,7 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
Layout::Custom(_) => None,
|
||||
})
|
||||
.flatten(),
|
||||
layout_options: value.layout_options(),
|
||||
custom_layout: value
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
@@ -305,10 +318,12 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.workspace_rules.clone()),
|
||||
work_area_offset: value.work_area_offset(),
|
||||
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
|
||||
window_container_behaviour: *value.window_container_behaviour(),
|
||||
window_container_behaviour_rules: Option::from(window_container_behaviour_rules),
|
||||
float_override: *value.float_override(),
|
||||
tile,
|
||||
layout_flip: value.layout_flip(),
|
||||
floating_layer_behaviour: value.floating_layer_behaviour(),
|
||||
wallpaper: None,
|
||||
@@ -397,7 +412,7 @@ pub enum AppSpecificConfigurationPath {
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.37`
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.38`
|
||||
pub struct StaticConfig {
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -807,7 +822,7 @@ pub struct StackbarConfig {
|
||||
/// Stackbar label
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub label: Option<StackbarLabel>,
|
||||
/// Stackbar mode
|
||||
/// Stackbar mode (default: Never)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<StackbarMode>,
|
||||
/// Stackbar tab configuration options
|
||||
@@ -1299,7 +1314,7 @@ impl StaticConfig {
|
||||
has_pending_raise_op: false,
|
||||
pending_move_op: Arc::new(None),
|
||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||
uncloak_to_ignore: 0,
|
||||
uncloack_to_ignore: 0,
|
||||
known_hwnds: HashMap::new(),
|
||||
};
|
||||
|
||||
@@ -1331,7 +1346,7 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
pub fn postload(path: &PathBuf, wm: &Arc<Mutex<WindowManager>>) -> Result<()> {
|
||||
let value = Self::read(path)?;
|
||||
let mut value = Self::read(path)?;
|
||||
let mut wm = wm.lock();
|
||||
|
||||
let configs_with_preference: Vec<_> =
|
||||
@@ -1342,6 +1357,8 @@ impl StaticConfig {
|
||||
workspace_matching_rules.clear();
|
||||
drop(workspace_matching_rules);
|
||||
|
||||
let monitor_count = wm.monitors().len();
|
||||
|
||||
let offset = wm.work_area_offset;
|
||||
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
|
||||
let preferred_config_idx = {
|
||||
@@ -1371,8 +1388,8 @@ impl StaticConfig {
|
||||
});
|
||||
if let Some(monitor_config) = value
|
||||
.monitors
|
||||
.as_ref()
|
||||
.and_then(|ms| idx.and_then(|i| ms.get(i)))
|
||||
.as_mut()
|
||||
.and_then(|ms| idx.and_then(|i| ms.get_mut(i)))
|
||||
{
|
||||
if let Some(used_config_idx) = idx {
|
||||
configs_used.push(used_config_idx);
|
||||
@@ -1395,7 +1412,14 @@ impl StaticConfig {
|
||||
|
||||
monitor.update_workspaces_globals(offset);
|
||||
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get_mut(j) {
|
||||
if monitor_count > 1
|
||||
&& matches!(workspace_config.layout, Some(DefaultLayout::Scrolling))
|
||||
{
|
||||
tracing::warn!("scrolling layout is only supported for a single monitor; falling back to columns layout");
|
||||
workspace_config.layout = Some(DefaultLayout::Columns);
|
||||
}
|
||||
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
@@ -1912,11 +1936,13 @@ mod tests {
|
||||
use crate::WorkspaceConfig;
|
||||
|
||||
#[test]
|
||||
#[ignore = "this fails on github actions due to rate limiting changes introduced in may 2025"]
|
||||
fn backwards_compat() {
|
||||
let root = vec!["0.1.17", "0.1.18", "0.1.19"];
|
||||
let docs = vec![
|
||||
"0.1.20", "0.1.21", "0.1.22", "0.1.23", "0.1.24", "0.1.25", "0.1.26", "0.1.27",
|
||||
"0.1.28", "0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33", "0.1.34",
|
||||
"0.1.28", "0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33", "0.1.34", "0.1.35",
|
||||
"0.1.36", "0.1.37",
|
||||
];
|
||||
|
||||
let mut versions = vec![];
|
||||
|
||||
@@ -120,13 +120,13 @@ pub struct WindowManager {
|
||||
pub has_pending_raise_op: bool,
|
||||
pub pending_move_op: Arc<Option<(usize, usize, isize)>>,
|
||||
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
|
||||
pub uncloak_to_ignore: usize,
|
||||
pub uncloack_to_ignore: usize,
|
||||
/// Maps each known window hwnd to the (monitor, workspace) index pair managing it
|
||||
pub known_hwnds: HashMap<isize, (usize, usize)>,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct State {
|
||||
pub monitors: Ring<Monitor>,
|
||||
@@ -329,6 +329,7 @@ impl From<&WindowManager> for State {
|
||||
maximized_window_restore_idx: workspace.maximized_window_restore_idx,
|
||||
floating_windows: workspace.floating_windows.clone(),
|
||||
layout: workspace.layout.clone(),
|
||||
layout_options: workspace.layout_options,
|
||||
layout_rules: workspace.layout_rules.clone(),
|
||||
layout_flip: workspace.layout_flip,
|
||||
workspace_padding: workspace.workspace_padding,
|
||||
@@ -336,6 +337,7 @@ impl From<&WindowManager> for State {
|
||||
latest_layout: workspace.latest_layout.clone(),
|
||||
resize_dimensions: workspace.resize_dimensions.clone(),
|
||||
tile: workspace.tile,
|
||||
work_area_offset: workspace.work_area_offset,
|
||||
apply_window_based_work_area_offset: workspace
|
||||
.apply_window_based_work_area_offset,
|
||||
window_container_behaviour: workspace.window_container_behaviour,
|
||||
@@ -346,7 +348,6 @@ impl From<&WindowManager> for State {
|
||||
layer: workspace.layer,
|
||||
floating_layer_behaviour: workspace.floating_layer_behaviour,
|
||||
globals: workspace.globals,
|
||||
locked_containers: workspace.locked_containers.clone(),
|
||||
wallpaper: workspace.wallpaper.clone(),
|
||||
workspace_config: None,
|
||||
})
|
||||
@@ -447,7 +448,7 @@ impl WindowManager {
|
||||
has_pending_raise_op: false,
|
||||
pending_move_op: Arc::new(None),
|
||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||
uncloak_to_ignore: 0,
|
||||
uncloack_to_ignore: 0,
|
||||
known_hwnds: HashMap::new(),
|
||||
})
|
||||
}
|
||||
@@ -821,7 +822,7 @@ impl WindowManager {
|
||||
target_workspace_idx: usize,
|
||||
floating: bool,
|
||||
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
|
||||
) -> () {
|
||||
) {
|
||||
tracing::trace!(
|
||||
"{} should be on monitor {}, workspace {}",
|
||||
window_title,
|
||||
@@ -1234,7 +1235,7 @@ impl WindowManager {
|
||||
// That workspace reconciliation would focus the window on the origin monitor.
|
||||
// So we need to ignore the uncloak events produced by the origin workspace
|
||||
// restore to avoid that issue.
|
||||
self.uncloak_to_ignore = uncloack_amount;
|
||||
self.uncloack_to_ignore = uncloack_amount;
|
||||
}
|
||||
} else if origin_workspace
|
||||
.maximized_window()
|
||||
@@ -1314,43 +1315,67 @@ impl WindowManager {
|
||||
let (origin_monitor_idx, origin_workspace_idx, origin_container_idx) = origin;
|
||||
let (target_monitor_idx, target_workspace_idx, target_container_idx) = target;
|
||||
|
||||
let origin_container = self
|
||||
let origin_container_is_valid = self
|
||||
.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
|
||||
.remove_container(origin_container_idx)
|
||||
.ok_or_else(|| anyhow!("there is no container at this index"))?;
|
||||
.containers()
|
||||
.get(origin_container_idx)
|
||||
.is_some();
|
||||
|
||||
let target_container = self
|
||||
let target_container_is_valid = self
|
||||
.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(target_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
|
||||
.remove_container(target_container_idx);
|
||||
.containers()
|
||||
.get(origin_container_idx)
|
||||
.is_some();
|
||||
|
||||
self.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(target_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
|
||||
.containers_mut()
|
||||
.insert(target_container_idx, origin_container);
|
||||
|
||||
if let Some(target_container) = target_container {
|
||||
self.monitors_mut()
|
||||
if origin_container_is_valid && target_container_is_valid {
|
||||
let origin_container = self
|
||||
.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
|
||||
.remove_container(origin_container_idx)
|
||||
.ok_or_else(|| anyhow!("there is no container at this index"))?;
|
||||
|
||||
let target_container = self
|
||||
.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(target_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
|
||||
.remove_container(target_container_idx);
|
||||
|
||||
self.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(target_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
|
||||
.containers_mut()
|
||||
.insert(origin_container_idx, target_container);
|
||||
.insert(target_container_idx, origin_container);
|
||||
|
||||
if let Some(target_container) = target_container {
|
||||
self.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this index"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace at this index"))?
|
||||
.containers_mut()
|
||||
.insert(origin_container_idx, target_container);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1555,6 +1580,9 @@ impl WindowManager {
|
||||
workspace.container_padding(),
|
||||
workspace.layout_flip(),
|
||||
&[],
|
||||
workspace.focused_container_idx(),
|
||||
workspace.layout_options(),
|
||||
workspace.latest_layout(),
|
||||
);
|
||||
|
||||
let mut direction = direction;
|
||||
@@ -2932,6 +2960,8 @@ impl WindowManager {
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn stack_all(&mut self) -> Result<()> {
|
||||
self.unstack_all(false)?;
|
||||
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
tracing::info!("stacking all windows on workspace");
|
||||
|
||||
@@ -2958,7 +2988,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn unstack_all(&mut self) -> Result<()> {
|
||||
pub fn unstack_all(&mut self, update_workspace: bool) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
tracing::info!("unstacking all windows in container");
|
||||
|
||||
@@ -2988,7 +3018,11 @@ impl WindowManager {
|
||||
workspace.focus_container_by_window(hwnd)?;
|
||||
}
|
||||
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
if update_workspace {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -3131,6 +3165,10 @@ impl WindowManager {
|
||||
pub fn toggle_float(&mut self, force_float: bool) -> Result<()> {
|
||||
let hwnd = WindowsApi::foreground_window()?;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if workspace.monocle_container().is_some() {
|
||||
tracing::warn!("ignoring toggle-float command while workspace has a monocle container");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut is_floating_window = false;
|
||||
|
||||
@@ -3154,14 +3192,10 @@ impl WindowManager {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn toggle_lock(&mut self) -> Result<()> {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let index = workspace.focused_container_idx();
|
||||
|
||||
if workspace.locked_containers().contains(&index) {
|
||||
workspace.locked_containers_mut().remove(&index);
|
||||
} else {
|
||||
workspace.locked_containers_mut().insert(index);
|
||||
if let Some(container) = workspace.focused_container_mut() {
|
||||
// Toggle the locked flag
|
||||
container.set_locked(!container.locked());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3324,8 +3358,16 @@ impl WindowManager {
|
||||
pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> Result<()> {
|
||||
tracing::info!("changing layout");
|
||||
|
||||
let monitor_count = self.monitors().len();
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
if monitor_count > 1 && matches!(layout, DefaultLayout::Scrolling) {
|
||||
tracing::warn!(
|
||||
"scrolling layout is only supported for a single monitor; not changing layout"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match workspace.layout() {
|
||||
Layout::Default(_) => {}
|
||||
Layout::Custom(layout) => {
|
||||
@@ -4288,6 +4330,44 @@ mod tests {
|
||||
assert_eq!(current_monitor_idx, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_focus_to_nonexistent_monitor() {
|
||||
let (mut wm, _test_context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a first monitor
|
||||
let m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// monitor should have a single workspace
|
||||
assert_eq!(m.workspaces().len(), 1);
|
||||
|
||||
// add the monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should have 1 monitor and the monitor index should be 0
|
||||
assert_eq!(wm.monitors().len(), 1);
|
||||
assert_eq!(wm.focused_monitor_idx(), 0);
|
||||
|
||||
// Should receive an error when trying to focus a non-existent monitor
|
||||
let result = wm.focus_monitor(1);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when focusing a non-existent monitor"
|
||||
);
|
||||
|
||||
// Should still be focused on the first monitor
|
||||
assert_eq!(wm.focused_monitor_idx(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_focused_monitor_size() {
|
||||
let (mut wm, _test_context) = setup_window_manager();
|
||||
@@ -4471,6 +4551,64 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_window_to_nonexistent_monitor() {
|
||||
// NOTE: transfer_window is primarily used when a window is being dragged by a mouse. The
|
||||
// transfer_window function does return an error when the target monitor doesn't exist but
|
||||
// there is a bug where the window isn't in the container after the window fails to
|
||||
// transfer. The test will test for the result of the transfer_window function but not if
|
||||
// the window is in the container after the transfer fails.
|
||||
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a first monitor
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Create a container
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
let mut container = Container::default();
|
||||
|
||||
// Add a window to the container
|
||||
container.windows_mut().push_back(Window::from(0));
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
// Should contain 1 container
|
||||
assert_eq!(workspace.containers().len(), 1);
|
||||
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
{
|
||||
// Monitor 0, Workspace 0, Window 0
|
||||
let origin = (0, 0, 0);
|
||||
|
||||
// Monitor 1, Workspace 0, Window 0
|
||||
//
|
||||
let target = (1, 0, 0);
|
||||
|
||||
// Attempt to transfer the window from monitor 0 to a non-existent monitor
|
||||
let result = wm.transfer_window(origin, target);
|
||||
|
||||
// Result should be an error since the monitor doesn't exist
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when transferring to a non-existent monitor"
|
||||
);
|
||||
|
||||
assert_eq!(wm.focused_container_idx().unwrap(), 0);
|
||||
assert_eq!(wm.focused_workspace_idx().unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_container() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -4635,6 +4773,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_nonexistent_window_from_container() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a first monitor
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor1".to_string(),
|
||||
"TestDevice1".to_string(),
|
||||
"TestDeviceID1".to_string(),
|
||||
Some("TestMonitorID1".to_string()),
|
||||
);
|
||||
|
||||
// Create a container
|
||||
let container = Container::default();
|
||||
|
||||
// Should have 3 windows in the container
|
||||
assert_eq!(container.windows().len(), 0);
|
||||
|
||||
// Add the container to a workspace
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should receive an error when trying to remove a window from an empty container
|
||||
let result = wm.remove_window_from_container();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to remove a window from an empty container"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_container_window_in_direction() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -4704,6 +4880,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_nonexistent_windows() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a first monitor
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor1".to_string(),
|
||||
"TestDevice1".to_string(),
|
||||
"TestDeviceID1".to_string(),
|
||||
Some("TestMonitorID1".to_string()),
|
||||
);
|
||||
|
||||
// Create a container
|
||||
let container = Container::default();
|
||||
|
||||
// Should have 3 windows in the container
|
||||
assert_eq!(container.windows().len(), 0);
|
||||
|
||||
// Add the container to a workspace
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should return an error when trying to cycle through windows in an empty container
|
||||
let result = wm.cycle_container_window_in_direction(CycleDirection::Next);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when cycling through windows in an empty container"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_container_window_index_in_direction() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -4862,6 +5076,71 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swap_container_with_nonexistent_container() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a first monitor
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Create a container
|
||||
let mut container = Container::default();
|
||||
|
||||
// Add three windows to the container
|
||||
for i in 0..3 {
|
||||
container.windows_mut().push_back(Window::from(i));
|
||||
}
|
||||
|
||||
// Should have 3 windows in the container
|
||||
assert_eq!(container.windows().len(), 3);
|
||||
|
||||
// Add the container to the workspace
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Monitor 0, Workspace 0, Window 0
|
||||
let origin = (0, 0, 0);
|
||||
|
||||
// Monitor 1, Workspace 0, Window 0
|
||||
let target = (0, 3, 0);
|
||||
|
||||
// Should be focused on the first container
|
||||
assert_eq!(wm.focused_container_idx().unwrap(), 0);
|
||||
|
||||
// Should return an error since there is only one container in the workspace
|
||||
let result = wm.swap_containers(origin, target);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when swapping with a non-existent container"
|
||||
);
|
||||
|
||||
// Should still be focused on the first container
|
||||
assert_eq!(wm.focused_container_idx().unwrap(), 0);
|
||||
|
||||
{
|
||||
// Should still have 1 container in the workspace
|
||||
let workspace = wm.focused_workspace_mut().unwrap();
|
||||
assert_eq!(workspace.containers().len(), 1);
|
||||
|
||||
// Container should still have 3 windows
|
||||
let container = workspace.focused_container_mut().unwrap();
|
||||
assert_eq!(container.windows().len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swap_monitor_workspaces() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -4946,6 +5225,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swap_workspace_with_nonexistent_monitor() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Add another workspace
|
||||
let new_workspace_index = m.new_workspace_idx();
|
||||
m.focus_workspace(new_workspace_index).unwrap();
|
||||
|
||||
// Should have 2 workspaces
|
||||
assert_eq!(m.workspaces().len(), 2);
|
||||
|
||||
// Add monitor to window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should be an error since Monitor 1 does not exist
|
||||
let result = wm.swap_monitor_workspaces(1, 0);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when swapping with a non-existent monitor"
|
||||
);
|
||||
|
||||
{
|
||||
// Should still have 2 workspaces in Monitor 0
|
||||
let monitor = wm.monitors().front().unwrap();
|
||||
let workspaces = monitor.workspaces();
|
||||
assert_eq!(
|
||||
workspaces.len(),
|
||||
2,
|
||||
"Expected 2 workspaces after swap attempt"
|
||||
);
|
||||
assert_eq!(wm.focused_monitor_idx(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_workspace_to_monitor() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -4968,7 +5293,7 @@ mod tests {
|
||||
// Should have 2 workspaces
|
||||
assert_eq!(m.workspaces().len(), 2);
|
||||
|
||||
// Add monitor to workspace
|
||||
// Add monitor to window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
@@ -5015,6 +5340,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_workspace_to_nonexistent_monitor() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Add another workspace
|
||||
let new_workspace_index = m.new_workspace_idx();
|
||||
m.focus_workspace(new_workspace_index).unwrap();
|
||||
|
||||
// Should have 2 workspaces
|
||||
assert_eq!(m.workspaces().len(), 2);
|
||||
|
||||
// Add monitor to window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Attempt to move a workspace to a non-existent monitor
|
||||
let result = wm.move_workspace_to_monitor(1);
|
||||
|
||||
// Should be an error since Monitor 1 does not exist
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when moving to a non-existent monitor"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_tiling() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -5086,7 +5447,8 @@ mod tests {
|
||||
{
|
||||
// Ensure container 2 is not locked
|
||||
let workspace = wm.focused_workspace_mut().unwrap();
|
||||
assert!(!workspace.locked_containers().contains(&2));
|
||||
assert_eq!(workspace.focused_container_idx(), 2);
|
||||
assert!(!workspace.focused_container().unwrap().locked());
|
||||
}
|
||||
|
||||
// Toggle lock on focused container
|
||||
@@ -5095,7 +5457,7 @@ mod tests {
|
||||
{
|
||||
// Ensure container 2 is locked
|
||||
let workspace = wm.focused_workspace_mut().unwrap();
|
||||
assert!(workspace.locked_containers().contains(&2));
|
||||
assert!(workspace.focused_container().unwrap().locked());
|
||||
}
|
||||
|
||||
// Toggle lock on focused container
|
||||
@@ -5104,7 +5466,7 @@ mod tests {
|
||||
{
|
||||
// Ensure container 2 is not locked
|
||||
let workspace = wm.focused_workspace_mut().unwrap();
|
||||
assert!(!workspace.locked_containers().contains(&2));
|
||||
assert!(!workspace.focused_container().unwrap().locked());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5186,6 +5548,40 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_nonexistent_window() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Add another workspace
|
||||
let new_workspace_index = m.new_workspace_idx();
|
||||
m.focus_workspace(new_workspace_index).unwrap();
|
||||
|
||||
// Should have 2 workspaces
|
||||
assert_eq!(m.workspaces().len(), 2);
|
||||
|
||||
// Add monitor to window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should return an error when trying to float a non-existent window
|
||||
let result = wm.float_window();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to float a non-existent window"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maximize_and_unmaximize_window() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -5332,6 +5728,41 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_maximize_nonexistent_window() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a monitor
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Create a container
|
||||
let container = Container::default();
|
||||
|
||||
// Add the container to the workspace
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should return an error when trying to toggle maximize on a non-existent window
|
||||
let result = wm.toggle_maximize();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to toggle maximize on a non-existent window"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_monocle_on_and_monocle_off() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -5403,6 +5834,41 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_monocle_on_and_off_nonexistent_container() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a monitor
|
||||
let m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should return an error when trying to move a non-existent container to monocle
|
||||
let result = wm.monocle_on();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to move a non-existent container to monocle"
|
||||
);
|
||||
|
||||
// Should return an error when trying to restore a non-existent container from monocle
|
||||
let result = wm.monocle_off();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to restore a non-existent container from monocle"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_monocle() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -5474,6 +5940,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_monocle_nonexistent_container() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a monitor
|
||||
let m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should return an error when trying to toggle monocle on a non-existent container
|
||||
let result = wm.toggle_monocle();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to toggle monocle on a non-existent container"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_named_workspace_for_monitor() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
@@ -1180,6 +1180,7 @@ impl WindowsApi {
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn enable_focus_follows_mouse() -> Result<()> {
|
||||
#[allow(clippy::manual_dangling_ptr)]
|
||||
Self::system_parameters_info_w(
|
||||
SPI_SETACTIVEWINDOWTRACKING,
|
||||
0,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Display;
|
||||
@@ -16,7 +15,8 @@ use crate::core::DefaultLayout;
|
||||
use crate::core::Layout;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
use crate::locked_deque::LockedDeque;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use crate::lockable_sequence::LockableSequence;
|
||||
use crate::ring::Ring;
|
||||
use crate::should_act;
|
||||
use crate::stackbar_manager;
|
||||
@@ -70,6 +70,8 @@ pub struct Workspace {
|
||||
pub floating_windows: Ring<Window>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub layout: Layout,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub layout_options: Option<LayoutOptions>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub layout_rules: Vec<(usize, Layout)>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
@@ -85,6 +87,8 @@ pub struct Workspace {
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub tile: bool,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub work_area_offset: Option<Rect>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub apply_window_based_work_area_offset: bool,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
@@ -100,8 +104,6 @@ pub struct Workspace {
|
||||
#[getset(get_copy = "pub", get_mut = "pub", set = "pub")]
|
||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub locked_containers: BTreeSet<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub wallpaper: Option<Wallpaper>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
@@ -139,6 +141,7 @@ impl Default for Workspace {
|
||||
monocle_container_restore_idx: None,
|
||||
floating_windows: Ring::default(),
|
||||
layout: Layout::Default(DefaultLayout::BSP),
|
||||
layout_options: None,
|
||||
layout_rules: vec![],
|
||||
layout_flip: None,
|
||||
workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)),
|
||||
@@ -146,6 +149,7 @@ impl Default for Workspace {
|
||||
latest_layout: vec![],
|
||||
resize_dimensions: vec![],
|
||||
tile: true,
|
||||
work_area_offset: None,
|
||||
apply_window_based_work_area_offset: true,
|
||||
window_container_behaviour: None,
|
||||
window_container_behaviour_rules: None,
|
||||
@@ -154,7 +158,6 @@ impl Default for Workspace {
|
||||
floating_layer_behaviour: Default::default(),
|
||||
globals: Default::default(),
|
||||
workspace_config: None,
|
||||
locked_containers: Default::default(),
|
||||
wallpaper: None,
|
||||
}
|
||||
}
|
||||
@@ -205,18 +208,16 @@ impl Workspace {
|
||||
|
||||
if let Some(layout) = &config.layout {
|
||||
self.layout = Layout::Default(*layout);
|
||||
self.tile = true;
|
||||
}
|
||||
|
||||
if let Some(pathbuf) = &config.custom_layout {
|
||||
let layout = CustomLayout::from_path(pathbuf)?;
|
||||
self.layout = Layout::Custom(layout);
|
||||
self.tile = true;
|
||||
}
|
||||
|
||||
if config.custom_layout.is_none() && config.layout.is_none() {
|
||||
self.tile = false;
|
||||
}
|
||||
self.tile =
|
||||
!(config.custom_layout.is_none() && config.layout.is_none() && config.tile.is_none()
|
||||
|| config.tile.is_some_and(|tile| !tile));
|
||||
|
||||
let mut all_layout_rules = vec![];
|
||||
if let Some(layout_rules) = &config.layout_rules {
|
||||
@@ -241,6 +242,8 @@ impl Workspace {
|
||||
self.set_layout_rules(all_layout_rules);
|
||||
}
|
||||
|
||||
self.set_work_area_offset(config.work_area_offset);
|
||||
|
||||
self.set_apply_window_based_work_area_offset(
|
||||
config.apply_window_based_work_area_offset.unwrap_or(true),
|
||||
);
|
||||
@@ -267,6 +270,7 @@ impl Workspace {
|
||||
self.set_layout_flip(config.layout_flip);
|
||||
self.set_floating_layer_behaviour(config.floating_layer_behaviour);
|
||||
self.set_wallpaper(config.wallpaper.clone());
|
||||
self.set_layout_options(config.layout_options);
|
||||
|
||||
self.set_workspace_config(Some(config.clone()));
|
||||
|
||||
@@ -495,7 +499,7 @@ impl Workspace {
|
||||
let border_width = self.globals().border_width;
|
||||
let border_offset = self.globals().border_offset;
|
||||
let work_area = self.globals().work_area;
|
||||
let work_area_offset = self.globals().work_area_offset;
|
||||
let work_area_offset = self.work_area_offset().or(self.globals().work_area_offset);
|
||||
let window_based_work_area_offset = self.globals().window_based_work_area_offset;
|
||||
let window_based_work_area_offset_limit =
|
||||
self.globals().window_based_work_area_offset_limit;
|
||||
@@ -583,6 +587,9 @@ impl Workspace {
|
||||
Some(container_padding),
|
||||
self.layout_flip(),
|
||||
self.resize_dimensions(),
|
||||
self.focused_container_idx(),
|
||||
self.layout_options(),
|
||||
self.latest_layout(),
|
||||
);
|
||||
|
||||
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
@@ -890,10 +897,9 @@ impl Workspace {
|
||||
// this fn respects locked container indexes - we should use it for pretty much everything
|
||||
// except monocle and maximize toggles
|
||||
pub fn insert_container_at_idx(&mut self, idx: usize, container: Container) -> usize {
|
||||
let mut locked_containers = self.locked_containers().clone();
|
||||
let mut ld = LockedDeque::new(self.containers_mut(), &mut locked_containers);
|
||||
let insertion_idx = ld.insert(idx, container);
|
||||
self.locked_containers = locked_containers;
|
||||
let insertion_idx = self
|
||||
.containers_mut()
|
||||
.insert_respecting_locks(idx, container);
|
||||
|
||||
if insertion_idx > self.resize_dimensions().len() {
|
||||
self.resize_dimensions_mut().push(None);
|
||||
@@ -909,10 +915,7 @@ impl Workspace {
|
||||
// this fn respects locked container indexes - we should use it for pretty much everything
|
||||
// except monocle and maximize toggles
|
||||
pub fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
|
||||
let mut locked_containers = self.locked_containers().clone();
|
||||
let mut ld = LockedDeque::new(self.containers_mut(), &mut locked_containers);
|
||||
let container = ld.remove(idx);
|
||||
self.locked_containers = locked_containers;
|
||||
let container = self.containers_mut().remove_respecting_locks(idx);
|
||||
|
||||
if idx < self.resize_dimensions().len() {
|
||||
self.resize_dimensions_mut().remove(idx);
|
||||
@@ -1194,6 +1197,9 @@ impl Workspace {
|
||||
Layout::Default(DefaultLayout::UltrawideVerticalStack) => {
|
||||
self.enforce_resize_for_ultrawide();
|
||||
}
|
||||
Layout::Default(DefaultLayout::Scrolling) => {
|
||||
self.enforce_resize_for_scrolling();
|
||||
}
|
||||
_ => self.enforce_no_resize(),
|
||||
}
|
||||
}
|
||||
@@ -1421,6 +1427,28 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_for_scrolling(&mut self) {
|
||||
let resize_dimensions = self.resize_dimensions_mut();
|
||||
match resize_dimensions.len() {
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
let len = resize_dimensions.len();
|
||||
|
||||
for (i, rect) in resize_dimensions.iter_mut().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
rect.top = 0;
|
||||
rect.bottom = 0;
|
||||
|
||||
if i == 0 {
|
||||
rect.left = 0;
|
||||
} else if i == len - 1 {
|
||||
rect.right = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn enforce_no_resize(&mut self) {
|
||||
for rect in self.resize_dimensions_mut().iter_mut().flatten() {
|
||||
rect.left = 0;
|
||||
@@ -1601,7 +1629,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn swap_containers(&mut self, i: usize, j: usize) {
|
||||
self.containers.swap(i, j);
|
||||
self.containers.elements_mut().swap_respecting_locks(i, j);
|
||||
self.focus_container(j);
|
||||
}
|
||||
|
||||
@@ -1700,7 +1728,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::container::Container;
|
||||
use crate::Window;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
@@ -1708,20 +1735,18 @@ mod tests {
|
||||
let mut ws = Workspace::default();
|
||||
|
||||
let mut state = HashMap::new();
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// add 3 containers
|
||||
// add 4 containers
|
||||
for i in 0..4 {
|
||||
let container = Container::default();
|
||||
let mut container = Container::default();
|
||||
if i == 3 {
|
||||
container.set_locked(true); // set index 3 locked
|
||||
}
|
||||
state.insert(i, container.id().to_string());
|
||||
ws.add_container_to_back(container);
|
||||
}
|
||||
assert_eq!(ws.containers().len(), 4);
|
||||
|
||||
// set index 3 locked
|
||||
locked.insert(3);
|
||||
ws.locked_containers = locked;
|
||||
|
||||
// focus container at index 2
|
||||
ws.focus_container(2);
|
||||
|
||||
@@ -1753,20 +1778,17 @@ mod tests {
|
||||
fn test_locked_containers_remove_window() {
|
||||
let mut ws = Workspace::default();
|
||||
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// add 4 containers
|
||||
for i in 0..4 {
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(Window::from(i));
|
||||
if i == 1 {
|
||||
container.set_locked(true);
|
||||
}
|
||||
ws.add_container_to_back(container);
|
||||
}
|
||||
assert_eq!(ws.containers().len(), 4);
|
||||
|
||||
// set index 1 locked
|
||||
locked.insert(1);
|
||||
ws.locked_containers = locked;
|
||||
|
||||
ws.remove_window(0).unwrap();
|
||||
assert_eq!(ws.containers()[0].focused_window().unwrap().hwnd, 2);
|
||||
// index 1 should still be the same
|
||||
@@ -1778,20 +1800,17 @@ mod tests {
|
||||
fn test_locked_containers_toggle_float() {
|
||||
let mut ws = Workspace::default();
|
||||
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// add 4 containers
|
||||
for i in 0..4 {
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(Window::from(i));
|
||||
if i == 1 {
|
||||
container.set_locked(true);
|
||||
}
|
||||
ws.add_container_to_back(container);
|
||||
}
|
||||
assert_eq!(ws.containers().len(), 4);
|
||||
|
||||
// set index 1 locked
|
||||
locked.insert(1);
|
||||
ws.locked_containers = locked;
|
||||
|
||||
// set index 0 focused
|
||||
ws.focus_container(0);
|
||||
|
||||
@@ -1823,20 +1842,17 @@ mod tests {
|
||||
fn test_locked_containers_stack() {
|
||||
let mut ws = Workspace::default();
|
||||
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// add 6 containers
|
||||
for i in 0..6 {
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(Window::from(i));
|
||||
if i == 4 {
|
||||
container.set_locked(true);
|
||||
}
|
||||
ws.add_container_to_back(container);
|
||||
}
|
||||
assert_eq!(ws.containers().len(), 6);
|
||||
|
||||
// set index 4 locked
|
||||
locked.insert(4);
|
||||
ws.locked_containers = locked;
|
||||
|
||||
// set index 3 focused
|
||||
ws.focus_container(3);
|
||||
|
||||
@@ -1939,6 +1955,33 @@ mod tests {
|
||||
assert_eq!(container.windows().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_non_existent_window() {
|
||||
let mut workspace = Workspace::default();
|
||||
|
||||
{
|
||||
// Add a container with one window
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(Window::from(1));
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
|
||||
// Attempt to remove a non-existent window
|
||||
let result = workspace.remove_window(2);
|
||||
|
||||
// Should return an error
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when removing a non-existent window"
|
||||
);
|
||||
|
||||
// Get focused container. Should be the index of the last container added
|
||||
let container = workspace.focused_container_mut().unwrap();
|
||||
|
||||
// Should still have 1 window
|
||||
assert_eq!(container.windows().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_focused_container() {
|
||||
let mut workspace = Workspace::default();
|
||||
@@ -2264,6 +2307,25 @@ mod tests {
|
||||
assert_eq!(container.windows().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_window_to_non_existent_container() {
|
||||
let mut workspace = Workspace::default();
|
||||
|
||||
// Add a container with one window
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(Window::from(1));
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
// Try to move window to a non-existent container
|
||||
let result = workspace.move_window_to_container(8);
|
||||
|
||||
// Should return an error
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when moving a window to a non-existent container"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_window() {
|
||||
let mut workspace = Workspace::default();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic-no-console"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
|
||||
@@ -184,6 +184,10 @@ MonitorWorkAreaOffset(monitor, left, top, right, bottom) {
|
||||
RunWait("komorebic.exe monitor-work-area-offset " monitor " " left " " top " " right " " bottom, , "Hide")
|
||||
}
|
||||
|
||||
WorkspaceWorkAreaOffset(monitor, workspace, left, top, right, bottom) {
|
||||
RunWait("komorebic.exe workspace-work-area-offset " monitor " "workspace" " left " " top " " right " " bottom, , "Hide")
|
||||
}
|
||||
|
||||
AdjustContainerPadding(sizing, adjustment) {
|
||||
RunWait("komorebic.exe adjust-container-padding " sizing " " adjustment, , "Hide")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
@@ -8,7 +8,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
@@ -36,7 +36,7 @@ shadow-rs = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["dep:schemars", "komorebi-client/schemars"]
|
||||
schemars = ["dep:schemars", "komorebi-client/default"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(FALSE)'] }
|
||||
@@ -6,9 +6,11 @@ use komorebi_client::replace_env_in_path;
|
||||
use komorebi_client::PathExt;
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@@ -91,8 +93,7 @@ lazy_static! {
|
||||
|
||||
assert!(
|
||||
whkd_config_home.is_dir(),
|
||||
"$Env:WHKD_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:WHKD_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
whkd_config_home
|
||||
@@ -428,6 +429,22 @@ struct MonitorWorkAreaOffset {
|
||||
bottom: i32,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct WorkspaceWorkAreaOffset {
|
||||
/// Monitor index (zero-indexed)
|
||||
monitor: usize,
|
||||
/// Workspace index (zero-indexed)
|
||||
workspace: usize,
|
||||
/// Size of the left work area offset (set right to left * 2 to maintain right padding)
|
||||
left: i32,
|
||||
/// Size of the top work area offset (set bottom to the same value to maintain bottom padding)
|
||||
top: i32,
|
||||
/// Size of the right work area offset
|
||||
right: i32,
|
||||
/// Size of the bottom work area offset
|
||||
bottom: i32,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct MonitorIndexPreference {
|
||||
/// Preferred monitor index (zero-indexed)
|
||||
@@ -963,6 +980,12 @@ struct EagerFocus {
|
||||
exe: String,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct ScrollingLayoutColumns {
|
||||
/// Desired number of visible columns
|
||||
count: NonZeroUsize,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
||||
struct Opts {
|
||||
@@ -1182,6 +1205,9 @@ enum SubCommand {
|
||||
/// Set offsets for a monitor to exclude parts of the work area from tiling
|
||||
#[clap(arg_required_else_help = true)]
|
||||
MonitorWorkAreaOffset(MonitorWorkAreaOffset),
|
||||
/// Set offsets for a workspace to exclude parts of the work area from tiling
|
||||
#[clap(arg_required_else_help = true)]
|
||||
WorkspaceWorkAreaOffset(WorkspaceWorkAreaOffset),
|
||||
/// Toggle application of the window-based work area offset for the focused workspace
|
||||
ToggleWindowBasedWorkAreaOffset,
|
||||
/// Set container padding on the focused workspace
|
||||
@@ -1202,6 +1228,9 @@ enum SubCommand {
|
||||
/// Cycle between available layouts
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleLayout(CycleLayout),
|
||||
/// Set the number of visible columns for the Scrolling layout on the focused workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
ScrollingLayoutColumns(ScrollingLayoutColumns),
|
||||
/// Load a custom layout from file for the focused workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
@@ -1298,7 +1327,7 @@ enum SubCommand {
|
||||
ToggleWorkspaceFloatOverride,
|
||||
/// Toggle between the Tiling and Floating layers on the focused workspace
|
||||
ToggleWorkspaceLayer,
|
||||
/// Toggle window tiling on the focused workspace
|
||||
/// Toggle the paused state for all window tiling
|
||||
TogglePause,
|
||||
/// Toggle window tiling on the focused workspace
|
||||
ToggleTiling,
|
||||
@@ -1552,6 +1581,33 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
SubCommand::Quickstart => {
|
||||
fn write_file_with_prompt(
|
||||
path: &PathBuf,
|
||||
content: &str,
|
||||
created_files: &mut Vec<String>,
|
||||
) -> Result<()> {
|
||||
if path.exists() {
|
||||
print!(
|
||||
"{} will be overwritten, do you want to continue? (y/N): ",
|
||||
path.display()
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
if trimmed == "y" || trimmed == "yes" {
|
||||
std::fs::write(path, content)?;
|
||||
created_files.push(path.display().to_string());
|
||||
} else {
|
||||
println!("Skipping {}", path.display());
|
||||
}
|
||||
} else {
|
||||
std::fs::write(path, content)?;
|
||||
created_files.push(path.display().to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let local_appdata_dir = data_local_dir().expect("could not find localdata dir");
|
||||
let data_dir = local_appdata_dir.join("komorebi");
|
||||
std::fs::create_dir_all(&*WHKD_CONFIG_DIR)?;
|
||||
@@ -1567,17 +1623,30 @@ fn main() -> Result<()> {
|
||||
komorebi_json.replace("Env:USERPROFILE", "Env:KOMOREBI_CONFIG_HOME");
|
||||
}
|
||||
|
||||
std::fs::write(HOME_DIR.join("komorebi.json"), komorebi_json)?;
|
||||
std::fs::write(HOME_DIR.join("komorebi.bar.json"), komorebi_bar_json)?;
|
||||
let komorebi_path = HOME_DIR.join("komorebi.json");
|
||||
let bar_path = HOME_DIR.join("komorebi.bar.json");
|
||||
let applications_path = HOME_DIR.join("applications.json");
|
||||
let whkdrc_path = WHKD_CONFIG_DIR.join("whkdrc");
|
||||
|
||||
let mut written_files = Vec::new();
|
||||
|
||||
write_file_with_prompt(&komorebi_path, &komorebi_json, &mut written_files)?;
|
||||
write_file_with_prompt(&bar_path, &komorebi_bar_json, &mut written_files)?;
|
||||
|
||||
let applications_json = include_str!("../applications.json");
|
||||
std::fs::write(HOME_DIR.join("applications.json"), applications_json)?;
|
||||
write_file_with_prompt(&applications_path, applications_json, &mut written_files)?;
|
||||
|
||||
let whkdrc = include_str!("../../docs/whkdrc.sample");
|
||||
std::fs::write(WHKD_CONFIG_DIR.join("whkdrc"), whkdrc)?;
|
||||
|
||||
println!("Example komorebi.json, komorebi.bar.json, whkdrc and latest applications.json files created");
|
||||
println!("You can now run komorebic start --whkd --bar");
|
||||
write_file_with_prompt(&whkdrc_path, whkdrc, &mut written_files)?;
|
||||
if written_files.is_empty() {
|
||||
println!("\nNo files were written.")
|
||||
} else {
|
||||
println!(
|
||||
"\nThe following example files were written:\n{}",
|
||||
written_files.join("\n")
|
||||
);
|
||||
}
|
||||
println!("\nYou can now run komorebic start --whkd --bar");
|
||||
}
|
||||
SubCommand::EnableAutostart(args) => {
|
||||
let mut current_exe = std::env::current_exe().expect("unable to get exec path");
|
||||
@@ -1929,6 +1998,20 @@ fn main() -> Result<()> {
|
||||
bottom: arg.bottom,
|
||||
}))?;
|
||||
}
|
||||
|
||||
SubCommand::WorkspaceWorkAreaOffset(arg) => {
|
||||
send_message(&SocketMessage::WorkspaceWorkAreaOffset(
|
||||
arg.monitor,
|
||||
arg.workspace,
|
||||
Rect {
|
||||
left: arg.left,
|
||||
top: arg.top,
|
||||
right: arg.right,
|
||||
bottom: arg.bottom,
|
||||
},
|
||||
))?;
|
||||
}
|
||||
|
||||
SubCommand::ToggleWindowBasedWorkAreaOffset => {
|
||||
send_message(&SocketMessage::ToggleWindowBasedWorkAreaOffset)?;
|
||||
}
|
||||
@@ -2625,6 +2708,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
||||
SubCommand::CycleLayout(arg) => {
|
||||
send_message(&SocketMessage::CycleLayout(arg.cycle_direction))?;
|
||||
}
|
||||
SubCommand::ScrollingLayoutColumns(arg) => {
|
||||
send_message(&SocketMessage::ScrollingLayoutColumns(arg.count))?;
|
||||
}
|
||||
SubCommand::LoadCustomLayout(arg) => {
|
||||
send_message(&SocketMessage::ChangeLayoutCustom(arg.path))?;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,6 @@ nav:
|
||||
- common-workflows/tray-and-multi-window-applications.md
|
||||
- common-workflows/mouse-follows-focus.md
|
||||
- common-workflows/dynamic-layout-switching.md
|
||||
- common-workflows/set-display-index.md
|
||||
- common-workflows/multiple-bar-instances.md
|
||||
- common-workflows/multi-monitor-setup.md
|
||||
- CLI reference:
|
||||
@@ -96,6 +95,7 @@ nav:
|
||||
- cli/state.md
|
||||
- cli/global-state.md
|
||||
- cli/gui.md
|
||||
- cli/toggle-shortcuts.md
|
||||
- cli/visible-windows.md
|
||||
- cli/monitor-information.md
|
||||
- cli/query.md
|
||||
@@ -158,6 +158,7 @@ nav:
|
||||
- cli/invisible-borders.md
|
||||
- cli/global-work-area-offset.md
|
||||
- cli/monitor-work-area-offset.md
|
||||
- cli/workspace-work-area-offset.md
|
||||
- cli/toggle-window-based-work-area-offset.md
|
||||
- cli/focused-workspace-container-padding.md
|
||||
- cli/focused-workspace-padding.md
|
||||
@@ -165,6 +166,7 @@ nav:
|
||||
- cli/adjust-workspace-padding.md
|
||||
- cli/change-layout.md
|
||||
- cli/cycle-layout.md
|
||||
- cli/scrolling-layout-columns.md
|
||||
- cli/flip-layout.md
|
||||
- cli/promote.md
|
||||
- cli/promote-focus.md
|
||||
|
||||
55320
schema.bar.json
55320
schema.bar.json
File diff suppressed because it is too large
Load Diff
69
schema.json
69
schema.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "StaticConfig",
|
||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.37`",
|
||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.38`",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation": {
|
||||
@@ -1790,7 +1790,8 @@
|
||||
"HorizontalStack",
|
||||
"UltrawideVerticalStack",
|
||||
"Grid",
|
||||
"RightMainVerticalStack"
|
||||
"RightMainVerticalStack",
|
||||
"Scrolling"
|
||||
]
|
||||
},
|
||||
"layout_flip": {
|
||||
@@ -1802,6 +1803,27 @@
|
||||
"HorizontalAndVertical"
|
||||
]
|
||||
},
|
||||
"layout_options": {
|
||||
"description": "Layout-specific options (default: None)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scrolling": {
|
||||
"description": "Options related to the Scrolling layout",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"columns"
|
||||
],
|
||||
"properties": {
|
||||
"columns": {
|
||||
"description": "Desired number of visible columns (default: 3)",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout_rules": {
|
||||
"description": "Layout rules in the format of threshold => layout (default: None)",
|
||||
"type": "object",
|
||||
@@ -1815,7 +1837,8 @@
|
||||
"HorizontalStack",
|
||||
"UltrawideVerticalStack",
|
||||
"Grid",
|
||||
"RightMainVerticalStack"
|
||||
"RightMainVerticalStack",
|
||||
"Scrolling"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -1823,6 +1846,10 @@
|
||||
"description": "Name",
|
||||
"type": "string"
|
||||
},
|
||||
"tile": {
|
||||
"description": "Enable or disable tiling for the workspace (default: true)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"wallpaper": {
|
||||
"description": "Specify a wallpaper for this workspace",
|
||||
"type": "object",
|
||||
@@ -2115,6 +2142,38 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"work_area_offset": {
|
||||
"description": "Workspace specific work area offset (default: None)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bottom",
|
||||
"left",
|
||||
"right",
|
||||
"top"
|
||||
],
|
||||
"properties": {
|
||||
"bottom": {
|
||||
"description": "The bottom point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"left": {
|
||||
"description": "The left point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"right": {
|
||||
"description": "The right point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"top": {
|
||||
"description": "The top point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace_padding": {
|
||||
"description": "Workspace padding (default: global)",
|
||||
"type": "integer",
|
||||
@@ -2498,7 +2557,7 @@
|
||||
]
|
||||
},
|
||||
"mode": {
|
||||
"description": "Stackbar mode",
|
||||
"description": "Stackbar mode (default: Never)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Always",
|
||||
@@ -4627,7 +4686,7 @@
|
||||
"description": "Which Windows signal to use when hiding windows (default: Cloak)",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)",
|
||||
"description": "END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Hide"
|
||||
|
||||
Reference in New Issue
Block a user