mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-05-30 15:30:44 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3281c61113 |
@@ -12,7 +12,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check and close feature issues
|
- name: Check and close feature issues
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const issue = context.payload.issue;
|
const issue = context.payload.issue;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
cargo-deny:
|
cargo-deny:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- run: rustup toolchain install stable --profile minimal
|
- run: rustup toolchain install stable --profile minimal
|
||||||
@@ -81,12 +81,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v4
|
||||||
- run: |
|
- run: |
|
||||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||||
@@ -128,14 +128,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ github.event.release.tag_name }}
|
TAG=${{ github.event.release.tag_name }}
|
||||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v4
|
||||||
- run: |
|
- run: |
|
||||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||||
@@ -170,14 +170,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ github.ref_name }}
|
TAG=${{ github.ref_name }}
|
||||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||||
- uses: actions/download-artifact@v5
|
- uses: actions/download-artifact@v4
|
||||||
- run: |
|
- run: |
|
||||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||||
|
|||||||
Generated
+985
-1111
File diff suppressed because it is too large
Load Diff
+2
-10
@@ -9,7 +9,7 @@ members = [
|
|||||||
"komorebic-no-console",
|
"komorebic-no-console",
|
||||||
"komorebi-bar",
|
"komorebi-bar",
|
||||||
"komorebi-themes",
|
"komorebi-themes",
|
||||||
"komorebi-shortcuts",
|
"komorebi-shortcuts"
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@@ -33,7 +33,6 @@ strum = { version = "0.27", features = ["derive"] }
|
|||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-appender = "0.2"
|
tracing-appender = "0.2"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
parking_lot = "0.12"
|
|
||||||
paste = "1"
|
paste = "1"
|
||||||
sysinfo = "0.34"
|
sysinfo = "0.34"
|
||||||
uds_windows = "1"
|
uds_windows = "1"
|
||||||
@@ -73,12 +72,5 @@ features = [
|
|||||||
"Win32_System_SystemServices",
|
"Win32_System_SystemServices",
|
||||||
"Win32_System_WindowsProgramming",
|
"Win32_System_WindowsProgramming",
|
||||||
"Media",
|
"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.
|
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.38"}
|
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.36"}
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use komorebi_client::Notification;
|
use komorebi_client::Notification;
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ feature-depth = 1
|
|||||||
ignore = [
|
ignore = [
|
||||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
||||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
|
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" }
|
||||||
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[licenses]
|
[licenses]
|
||||||
|
|||||||
+293
-301
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
|
|||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
<DEFAULT_LAYOUT>
|
<DEFAULT_LAYOUT>
|
||||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help
|
-h, --help
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Arguments:
|
|||||||
The number of window containers on-screen required to trigger this layout rule
|
The number of window containers on-screen required to trigger this layout rule
|
||||||
|
|
||||||
<LAYOUT>
|
<LAYOUT>
|
||||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help
|
-h, --help
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Arguments:
|
|||||||
Target workspace name
|
Target workspace name
|
||||||
|
|
||||||
<VALUE>
|
<VALUE>
|
||||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help
|
-h, --help
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ Usage: komorebic.exe query <STATE_QUERY>
|
|||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
<STATE_QUERY>
|
<STATE_QUERY>
|
||||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, focused-container-kind, version]
|
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, version]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help
|
-h, --help
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# scrolling-layout-columns
|
|
||||||
|
|
||||||
```
|
|
||||||
Set the number of visible columns for the Scrolling layout on the focused workspace
|
|
||||||
|
|
||||||
Usage: komorebic.exe scrolling-layout-columns <COUNT>
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
<COUNT>
|
|
||||||
Desired number of visible columns
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h, --help
|
|
||||||
Print help
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# toggle-pause
|
# toggle-pause
|
||||||
|
|
||||||
```
|
```
|
||||||
Toggle the paused state for all window tiling
|
Toggle window tiling on the focused workspace
|
||||||
|
|
||||||
Usage: komorebic.exe toggle-pause
|
Usage: komorebic.exe toggle-pause
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
# toggle-shortcuts
|
|
||||||
|
|
||||||
```
|
|
||||||
Toggle the komorebi-shortcuts helper
|
|
||||||
|
|
||||||
Usage: komorebic.exe toggle-shortcuts
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h, --help
|
|
||||||
Print help
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -8,7 +8,7 @@ Usage: komorebic.exe window-hiding-behaviour <HIDING_BEHAVIOUR>
|
|||||||
Arguments:
|
Arguments:
|
||||||
<HIDING_BEHAVIOUR>
|
<HIDING_BEHAVIOUR>
|
||||||
Possible values:
|
Possible values:
|
||||||
- hide: END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||||
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
- 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
|
- 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
|
The number of window containers on-screen required to trigger this layout rule
|
||||||
|
|
||||||
<LAYOUT>
|
<LAYOUT>
|
||||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help
|
-h, --help
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Arguments:
|
|||||||
Workspace index on the specified monitor (zero-indexed)
|
Workspace index on the specified monitor (zero-indexed)
|
||||||
|
|
||||||
<VALUE>
|
<VALUE>
|
||||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help
|
-h, --help
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
# workspace-work-area-offset
|
|
||||||
|
|
||||||
```
|
|
||||||
Set offsets for a workspace to exclude parts of the work area from tiling
|
|
||||||
|
|
||||||
Usage: komorebic.exe workspace-work-area-offset <MONITOR> <WORKSPACE> <LEFT> <TOP> <RIGHT> <BOTTOM>
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
<MONITOR>
|
|
||||||
Monitor index (zero-indexed)
|
|
||||||
|
|
||||||
<WORKSPACE>
|
|
||||||
Workspace index (zero-indexed)
|
|
||||||
|
|
||||||
<LEFT>
|
|
||||||
Size of the left work area offset (set right to left * 2 to maintain right padding)
|
|
||||||
|
|
||||||
<TOP>
|
|
||||||
Size of the top work area offset (set bottom to the same value to maintain bottom padding)
|
|
||||||
|
|
||||||
<RIGHT>
|
|
||||||
Size of the right work area offset
|
|
||||||
|
|
||||||
<BOTTOM>
|
|
||||||
Size of the bottom work area offset
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h, --help
|
|
||||||
Print help
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -6,10 +6,7 @@ defined in the `komorebi.json` configuration file.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"animation": {
|
"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.
|
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.
|
However, if you do this it is important to be aware of a few things.
|
||||||
@@ -393,13 +393,6 @@ This is because komorebi will apply the appropriate config to the loaded monitor
|
|||||||
index (the index defined in the user config) to the actual monitor index, and the bar will use that map to know if it
|
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.
|
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
|
### 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
|
* If you are using a laptop connected to one monitor at work and a different one at home, the work monitor and the home
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Setting a Given Display to a Specific Index
|
||||||
|
|
||||||
|
If you would like `komorebi` to remember monitor index positions, you will need to set the `display_index_preferences`
|
||||||
|
configuration option in the static configuration file.
|
||||||
|
|
||||||
|
Display IDs can be found using `komorebic monitor-information`.
|
||||||
|
|
||||||
|
Then, in `komorebi.json`, you simply need to specify the preferred index position for each display ID:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"display_index_preferences": {
|
||||||
|
"0": "DEL4310-5&1a6c0954&0&UID209155",
|
||||||
|
"1": "<another-display_id>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -186,9 +186,6 @@ limitations on hotkey bindings that include the `win` key. However, you will sti
|
|||||||
to [modify the registry](https://superuser.com/questions/1059511/how-to-disable-winl-in-windows-10) to prevent
|
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.
|
`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" %}
|
{% include "./whkdrc.sample" %}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ cargo +stable install --path komorebic --locked
|
|||||||
cargo +stable install --path komorebic-no-console --locked
|
cargo +stable install --path komorebic-no-console --locked
|
||||||
cargo +stable install --path komorebi-gui --locked
|
cargo +stable install --path komorebi-gui --locked
|
||||||
cargo +stable install --path komorebi-bar --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
|
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.38/schema.bar.json",
|
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.36/schema.bar.json",
|
||||||
"monitor": 0,
|
"monitor": 0,
|
||||||
"font_family": "JetBrains Mono",
|
"font_family": "JetBrains Mono",
|
||||||
"theme": {
|
"theme": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.38/schema.json",
|
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.36/schema.json",
|
||||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||||
"window_hiding_behaviour": "Cloak",
|
"window_hiding_behaviour": "Cloak",
|
||||||
"cross_monitor_move_behaviour": "Insert",
|
"cross_monitor_move_behaviour": "Insert",
|
||||||
|
|||||||
@@ -138,14 +138,13 @@ running `komorebic stop` and `komorebic start`.
|
|||||||
Users with Nvidia GPUs may have issues with transparency on the Komorebi Bar.
|
Users with Nvidia GPUs may have issues with transparency on the Komorebi Bar.
|
||||||
|
|
||||||
To solve this the user can do the following:
|
To solve this the user can do the following:
|
||||||
|
1. Open the Nvidia Control Panel
|
||||||
- Open the Nvidia Control Panel
|
2. On the left menu tree, under "3D Settings", select "Manage 3D Settings"
|
||||||
- On the left menu tree, under "3D Settings", select "Manage 3D Settings"
|
3. Select the "Program Settings" tab
|
||||||
- Select the "Program Settings" tab
|
4. Press the "Add" button and select "komorebi-bar"
|
||||||
- Press the "Add" button and select "komorebi-bar"
|
5. Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
|
||||||
- Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
|
6. Change the setting to "Prefer compatibility"
|
||||||
- Change the setting to "Prefer compatibility"
|
7. At the bottom of the window select "Apply"
|
||||||
- At the bottom of the window select "Apply"
|
8. Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
|
||||||
- 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.
|
This should resolve the issue and your Komorebi Bar should render with the proper transparency.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ install-target-with-jsonschema target:
|
|||||||
cargo +stable install --path {{ target }} --locked
|
cargo +stable install --path {{ target }} --locked
|
||||||
|
|
||||||
install:
|
install:
|
||||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||||
|
|
||||||
install-with-jsonschema:
|
install-with-jsonschema:
|
||||||
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||||
@@ -52,7 +52,7 @@ wpm target:
|
|||||||
just build-target {{ target }} && wpmctl stop {{ target }}; just copy-target {{ target }} && wpmctl start {{ target }}
|
just build-target {{ target }} && wpmctl stop {{ target }}; just copy-target {{ target }} && wpmctl start {{ target }}
|
||||||
|
|
||||||
copy:
|
copy:
|
||||||
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||||
|
|
||||||
run target:
|
run target:
|
||||||
cargo +stable run --bin {{ target }} --locked --no-default-features
|
cargo +stable run --bin {{ target }} --locked --no-default-features
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-bar"
|
name = "komorebi-bar"
|
||||||
version = "0.1.38"
|
version = "0.1.37"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
komorebi-client = { path = "../komorebi-client" }
|
||||||
komorebi-themes = { path = "../komorebi-themes", default-features = false }
|
komorebi-themes = { path = "../komorebi-themes" }
|
||||||
|
|
||||||
chrono-tz = { workspace = true }
|
chrono-tz = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
@@ -22,11 +22,10 @@ font-loader = "0.11"
|
|||||||
hotwatch = { workspace = true }
|
hotwatch = { workspace = true }
|
||||||
image = "0.25"
|
image = "0.25"
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
netdev = "0.36"
|
netdev = "0.34"
|
||||||
num = "0.4"
|
num = "0.4"
|
||||||
num-derive = "0.4"
|
num-derive = "0.4"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
parking_lot = { workspace = true }
|
|
||||||
random_word = { version = "0.5", features = ["en"] }
|
random_word = { version = "0.5", features = ["en"] }
|
||||||
reqwest = { version = "0.12", features = ["blocking"] }
|
reqwest = { version = "0.12", features = ["blocking"] }
|
||||||
schemars = { workspace = true, optional = true }
|
schemars = { workspace = true, optional = true }
|
||||||
@@ -44,4 +43,4 @@ windows-icons-fallback = { package = "windows-icons", git = "https://github.com/
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["schemars"]
|
default = ["schemars"]
|
||||||
schemars = ["dep:schemars", "komorebi-client/default", "komorebi-themes/default"]
|
schemars = ["dep:schemars", "komorebi-client/schemars", "komorebi-themes/schemars"]
|
||||||
|
|||||||
+60
-338
@@ -10,7 +10,7 @@ use crate::render::Grouping;
|
|||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::render::RenderExt;
|
use crate::render::RenderExt;
|
||||||
use crate::widgets::komorebi::Komorebi;
|
use crate::widgets::komorebi::Komorebi;
|
||||||
use crate::widgets::komorebi::MonitorInfo;
|
use crate::widgets::komorebi::KomorebiNotificationState;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
use crate::widgets::widget::WidgetConfig;
|
use crate::widgets::widget::WidgetConfig;
|
||||||
use crate::KomorebiEvent;
|
use crate::KomorebiEvent;
|
||||||
@@ -38,7 +38,6 @@ use eframe::egui::Frame;
|
|||||||
use eframe::egui::Id;
|
use eframe::egui::Id;
|
||||||
use eframe::egui::Layout;
|
use eframe::egui::Layout;
|
||||||
use eframe::egui::Margin;
|
use eframe::egui::Margin;
|
||||||
use eframe::egui::PointerButton;
|
|
||||||
use eframe::egui::Rgba;
|
use eframe::egui::Rgba;
|
||||||
use eframe::egui::Style;
|
use eframe::egui::Style;
|
||||||
use eframe::egui::TextStyle;
|
use eframe::egui::TextStyle;
|
||||||
@@ -47,110 +46,31 @@ use eframe::egui::Visuals;
|
|||||||
use font_loader::system_fonts;
|
use font_loader::system_fonts;
|
||||||
use font_loader::system_fonts::FontPropertyBuilder;
|
use font_loader::system_fonts::FontPropertyBuilder;
|
||||||
use komorebi_client::Colour;
|
use komorebi_client::Colour;
|
||||||
|
use komorebi_client::KomorebiTheme;
|
||||||
use komorebi_client::MonitorNotification;
|
use komorebi_client::MonitorNotification;
|
||||||
use komorebi_client::NotificationEvent;
|
use komorebi_client::NotificationEvent;
|
||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use komorebi_client::SocketMessage;
|
use komorebi_client::SocketMessage;
|
||||||
use komorebi_client::VirtualDesktopNotification;
|
use komorebi_client::VirtualDesktopNotification;
|
||||||
use komorebi_themes::catppuccin_egui;
|
use komorebi_themes::catppuccin_egui;
|
||||||
|
use komorebi_themes::Base16Value;
|
||||||
use komorebi_themes::Base16Wrapper;
|
use komorebi_themes::Base16Wrapper;
|
||||||
use komorebi_themes::Catppuccin;
|
use komorebi_themes::Catppuccin;
|
||||||
use lazy_static::lazy_static;
|
use komorebi_themes::CatppuccinValue;
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Error;
|
|
||||||
use std::io::ErrorKind;
|
|
||||||
use std::io::Result;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::ChildStdin;
|
|
||||||
use std::process::Command;
|
|
||||||
use std::process::Stdio;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
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 struct Komobar {
|
||||||
pub hwnd: Option<isize>,
|
pub hwnd: Option<isize>,
|
||||||
pub monitor_index: Option<usize>,
|
pub monitor_index: Option<usize>,
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
pub config: KomobarConfig,
|
pub config: KomobarConfig,
|
||||||
pub render_config: Rc<RefCell<RenderConfig>>,
|
pub render_config: Rc<RefCell<RenderConfig>>,
|
||||||
pub monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||||
pub center_widgets: Vec<Box<dyn BarWidget>>,
|
pub center_widgets: Vec<Box<dyn BarWidget>>,
|
||||||
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
||||||
@@ -162,18 +82,6 @@ pub struct Komobar {
|
|||||||
pub size_rect: komorebi_client::Rect,
|
pub size_rect: komorebi_client::Rect,
|
||||||
pub work_area_offset: komorebi_client::Rect,
|
pub work_area_offset: komorebi_client::Rect,
|
||||||
applied_theme_on_first_frame: bool,
|
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(
|
pub fn apply_theme(
|
||||||
@@ -342,7 +250,7 @@ impl Komobar {
|
|||||||
pub fn apply_config(
|
pub fn apply_config(
|
||||||
&mut self,
|
&mut self,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
previous_monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||||
) {
|
) {
|
||||||
MAX_LABEL_WIDTH.store(
|
MAX_LABEL_WIDTH.store(
|
||||||
self.config.max_label_width.unwrap_or(400.0) as i32,
|
self.config.max_label_width.unwrap_or(400.0) as i32,
|
||||||
@@ -371,7 +279,7 @@ impl Komobar {
|
|||||||
self.config.icon_scale,
|
self.config.icon_scale,
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut monitor_info = previous_monitor_info;
|
let mut komorebi_notification_state = previous_notification_state;
|
||||||
let mut komorebi_widgets = Vec::new();
|
let mut komorebi_widgets = Vec::new();
|
||||||
|
|
||||||
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
|
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
|
||||||
@@ -423,18 +331,19 @@ impl Komobar {
|
|||||||
komorebi_widgets
|
komorebi_widgets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.for_each(|(mut widget, idx, side)| {
|
.for_each(|(mut widget, idx, side)| {
|
||||||
match monitor_info {
|
match komorebi_notification_state {
|
||||||
None => {
|
None => {
|
||||||
monitor_info = Some(widget.monitor_info.clone());
|
komorebi_notification_state =
|
||||||
|
Some(widget.komorebi_notification_state.clone());
|
||||||
}
|
}
|
||||||
Some(ref previous) => {
|
Some(ref previous) => {
|
||||||
if widget.workspaces.is_some() {
|
if widget.workspaces.is_some_and(|w| w.enable) {
|
||||||
previous
|
previous.borrow_mut().update_from_config(
|
||||||
.borrow_mut()
|
&widget.komorebi_notification_state.borrow(),
|
||||||
.update_from_self(&widget.monitor_info.borrow());
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
widget.monitor_info = previous.clone();
|
widget.komorebi_notification_state = previous.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,19 +368,15 @@ impl Komobar {
|
|||||||
}
|
}
|
||||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||||
};
|
};
|
||||||
|
let monitor_index = self.komorebi_notification_state.as_ref().and_then(|state| {
|
||||||
let mapped_info = self.monitor_info.as_ref().map(|info| {
|
state
|
||||||
let monitor = info.borrow();
|
.borrow()
|
||||||
(
|
.monitor_usr_idx_map
|
||||||
monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
|
.get(&usr_monitor_index)
|
||||||
monitor.mouse_follows_focus,
|
.copied()
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(info) = mapped_info {
|
self.monitor_index = monitor_index;
|
||||||
self.monitor_index = info.0;
|
|
||||||
self.mouse_follows_focus = info.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(monitor_index) = self.monitor_index {
|
if let Some(monitor_index) = self.monitor_index {
|
||||||
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
|
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
|
||||||
@@ -522,7 +427,7 @@ impl Komobar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if self.monitor_info.is_some() && !self.disabled {
|
} else if self.komorebi_notification_state.is_some() && !self.disabled {
|
||||||
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
|
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
|
||||||
self.disabled = true;
|
self.disabled = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -530,39 +435,9 @@ impl Komobar {
|
|||||||
self.disabled = true;
|
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");
|
tracing::info!("widget configuration options applied");
|
||||||
|
|
||||||
self.monitor_info = monitor_info;
|
self.komorebi_notification_state = komorebi_notification_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
|
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
|
||||||
@@ -631,7 +506,8 @@ impl Komobar {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
home.is_dir(),
|
home.is_dir(),
|
||||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||||
|
home_path
|
||||||
);
|
);
|
||||||
|
|
||||||
home
|
home
|
||||||
@@ -645,6 +521,26 @@ impl Komobar {
|
|||||||
match komorebi_client::StaticConfig::read(&config) {
|
match komorebi_client::StaticConfig::read(&config) {
|
||||||
Ok(config) => {
|
Ok(config) => {
|
||||||
if let Some(theme) = config.theme {
|
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(
|
apply_theme(
|
||||||
ctx,
|
ctx,
|
||||||
KomobarTheme::from(theme),
|
KomobarTheme::from(theme),
|
||||||
@@ -654,6 +550,10 @@ impl Komobar {
|
|||||||
bar_grouping,
|
bar_grouping,
|
||||||
self.render_config.clone(),
|
self.render_config.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some(state) = &self.komorebi_notification_state {
|
||||||
|
state.borrow_mut().stack_accent = Some(stack_accent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -696,7 +596,7 @@ impl Komobar {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
config,
|
config,
|
||||||
render_config: Rc::new(RefCell::new(RenderConfig::new())),
|
render_config: Rc::new(RefCell::new(RenderConfig::new())),
|
||||||
monitor_info: None,
|
komorebi_notification_state: None,
|
||||||
left_widgets: vec![],
|
left_widgets: vec![],
|
||||||
center_widgets: vec![],
|
center_widgets: vec![],
|
||||||
right_widgets: vec![],
|
right_widgets: vec![],
|
||||||
@@ -708,16 +608,6 @@ impl Komobar {
|
|||||||
size_rect: komorebi_client::Rect::default(),
|
size_rect: komorebi_client::Rect::default(),
|
||||||
work_area_offset: komorebi_client::Rect::default(),
|
work_area_offset: komorebi_client::Rect::default(),
|
||||||
applied_theme_on_first_frame: false,
|
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);
|
komobar.apply_config(&cc.egui_ctx, None);
|
||||||
@@ -842,12 +732,12 @@ impl eframe::App for Komobar {
|
|||||||
|
|
||||||
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
|
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.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
|
||||||
self.apply_config(ctx, self.monitor_info.clone());
|
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(updated_config) = self.rx_config.try_recv() {
|
if let Ok(updated_config) = self.rx_config.try_recv() {
|
||||||
self.config = updated_config;
|
self.config = updated_config;
|
||||||
self.apply_config(ctx, self.monitor_info.clone());
|
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.rx_gui.try_recv() {
|
match self.rx_gui.try_recv() {
|
||||||
@@ -972,15 +862,13 @@ impl eframe::App for Komobar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(monitor_info) = &self.monitor_info {
|
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
|
||||||
monitor_info.borrow_mut().update(
|
komorebi_notification_state
|
||||||
self.monitor_index,
|
.borrow_mut()
|
||||||
notification.state,
|
.handle_notification(
|
||||||
self.render_config.borrow().show_all_icons,
|
|
||||||
);
|
|
||||||
handle_notification(
|
|
||||||
ctx,
|
ctx,
|
||||||
notification.event,
|
self.monitor_index,
|
||||||
|
notification,
|
||||||
self.bg_color.clone(),
|
self.bg_color.clone(),
|
||||||
self.bg_color_with_alpha.clone(),
|
self.bg_color_with_alpha.clone(),
|
||||||
self.config.transparency_alpha,
|
self.config.transparency_alpha,
|
||||||
@@ -991,7 +879,7 @@ impl eframe::App for Komobar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if should_apply_config {
|
if should_apply_config {
|
||||||
self.apply_config(ctx, self.monitor_info.clone());
|
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||||
|
|
||||||
// Reposition the Bar
|
// Reposition the Bar
|
||||||
self.position_bar();
|
self.position_bar();
|
||||||
@@ -1073,111 +961,6 @@ impl eframe::App for Komobar {
|
|||||||
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
||||||
|
|
||||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||||
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
|
// Apply grouping logic for the bar as a whole
|
||||||
let area_frame = if let Some(frame) = &self.config.frame {
|
let area_frame = if let Some(frame) = &self.config.frame {
|
||||||
Frame::NONE
|
Frame::NONE
|
||||||
@@ -1317,64 +1100,3 @@ pub enum Alignment {
|
|||||||
Center,
|
Center,
|
||||||
Right,
|
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
-146
@@ -1,4 +1,3 @@
|
|||||||
use crate::bar::exec_powershell;
|
|
||||||
use crate::render::Grouping;
|
use crate::render::Grouping;
|
||||||
use crate::widgets::widget::WidgetConfig;
|
use crate::widgets::widget::WidgetConfig;
|
||||||
use crate::DEFAULT_PADDING;
|
use crate::DEFAULT_PADDING;
|
||||||
@@ -6,9 +5,7 @@ use eframe::egui::Pos2;
|
|||||||
use eframe::egui::TextBuffer;
|
use eframe::egui::TextBuffer;
|
||||||
use eframe::egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
use komorebi_client::KomorebiTheme;
|
use komorebi_client::KomorebiTheme;
|
||||||
use komorebi_client::PathExt;
|
|
||||||
use komorebi_client::Rect;
|
use komorebi_client::Rect;
|
||||||
use komorebi_client::SocketMessage;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -16,7 +13,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.38`
|
/// The `komorebi.bar.json` configuration file reference for `v0.1.37`
|
||||||
pub struct KomobarConfig {
|
pub struct KomobarConfig {
|
||||||
/// Bar height (default: 50)
|
/// Bar height (default: 50)
|
||||||
pub height: Option<f32>,
|
pub height: Option<f32>,
|
||||||
@@ -93,8 +90,6 @@ pub struct KomobarConfig {
|
|||||||
pub widget_spacing: Option<f32>,
|
pub widget_spacing: Option<f32>,
|
||||||
/// Visual grouping for widgets
|
/// Visual grouping for widgets
|
||||||
pub grouping: Option<Grouping>,
|
pub grouping: Option<Grouping>,
|
||||||
/// Options for mouse interaction on the bar
|
|
||||||
pub mouse: Option<MouseConfig>,
|
|
||||||
/// Left side widgets (ordered left-to-right)
|
/// Left side widgets (ordered left-to-right)
|
||||||
pub left_widgets: Vec<WidgetConfig>,
|
pub left_widgets: Vec<WidgetConfig>,
|
||||||
/// Center widgets (ordered left-to-right)
|
/// Center widgets (ordered left-to-right)
|
||||||
@@ -330,146 +325,6 @@ pub fn get_individual_spacing(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum MouseMessage {
|
|
||||||
/// Send a message to the komorebi client.
|
|
||||||
/// By default, a batch of messages are sent in the following order:
|
|
||||||
/// FocusMonitorAtCursor =>
|
|
||||||
/// MouseFollowsFocus(false) =>
|
|
||||||
/// {message} =>
|
|
||||||
/// MouseFollowsFocus({original.value})
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```json
|
|
||||||
/// "on_extra2_click": {
|
|
||||||
/// "message": {
|
|
||||||
/// "type": "NewWorkspace"
|
|
||||||
/// }
|
|
||||||
/// },
|
|
||||||
/// ```
|
|
||||||
/// or:
|
|
||||||
/// ```json
|
|
||||||
/// "on_middle_click": {
|
|
||||||
/// "focus_monitor_at_cursor": false,
|
|
||||||
/// "ignore_mouse_follows_focus": false,
|
|
||||||
/// "message": {
|
|
||||||
/// "type": "TogglePause"
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// or:
|
|
||||||
/// ```json
|
|
||||||
/// "on_scroll_up": {
|
|
||||||
/// "message": {
|
|
||||||
/// "type": "CycleFocusWorkspace",
|
|
||||||
/// "content": "Previous"
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
Komorebi(KomorebiMouseMessage),
|
|
||||||
/// Execute a custom command.
|
|
||||||
/// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved.
|
|
||||||
/// Example: `komorebic toggle-pause`
|
|
||||||
Command(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
pub struct KomorebiMouseMessage {
|
|
||||||
/// Send the FocusMonitorAtCursor message (default:true)
|
|
||||||
pub focus_monitor_at_cursor: Option<bool>,
|
|
||||||
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message (default:true)
|
|
||||||
pub ignore_mouse_follows_focus: Option<bool>,
|
|
||||||
/// The message to send to the komorebi client
|
|
||||||
pub message: komorebi_client::SocketMessage,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
||||||
pub struct MouseConfig {
|
|
||||||
/// Command to send on primary/left double button click
|
|
||||||
pub on_primary_double_click: Option<MouseMessage>,
|
|
||||||
/// Command to send on secondary/right button click
|
|
||||||
pub on_secondary_click: Option<MouseMessage>,
|
|
||||||
/// Command to send on middle button click
|
|
||||||
pub on_middle_click: Option<MouseMessage>,
|
|
||||||
/// Command to send on extra1/back button click
|
|
||||||
pub on_extra1_click: Option<MouseMessage>,
|
|
||||||
/// Command to send on extra2/forward button click
|
|
||||||
pub on_extra2_click: Option<MouseMessage>,
|
|
||||||
|
|
||||||
/// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
|
||||||
pub vertical_scroll_threshold: Option<f32>,
|
|
||||||
/// Command to send on scrolling up (every tick)
|
|
||||||
pub on_scroll_up: Option<MouseMessage>,
|
|
||||||
/// Command to send on scrolling down (every tick)
|
|
||||||
pub on_scroll_down: Option<MouseMessage>,
|
|
||||||
|
|
||||||
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
|
||||||
pub horizontal_scroll_threshold: Option<f32>,
|
|
||||||
/// Command to send on scrolling left (every tick)
|
|
||||||
pub on_scroll_left: Option<MouseMessage>,
|
|
||||||
/// Command to send on scrolling right (every tick)
|
|
||||||
pub on_scroll_right: Option<MouseMessage>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MouseConfig {
|
|
||||||
pub fn has_command(&self) -> bool {
|
|
||||||
[
|
|
||||||
&self.on_primary_double_click,
|
|
||||||
&self.on_secondary_click,
|
|
||||||
&self.on_middle_click,
|
|
||||||
&self.on_extra1_click,
|
|
||||||
&self.on_extra2_click,
|
|
||||||
&self.on_scroll_up,
|
|
||||||
&self.on_scroll_down,
|
|
||||||
&self.on_scroll_left,
|
|
||||||
&self.on_scroll_right,
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.any(|opt| matches!(opt, Some(MouseMessage::Command(_))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MouseMessage {
|
|
||||||
pub fn execute(&self, mouse_follows_focus: bool) {
|
|
||||||
match self {
|
|
||||||
MouseMessage::Komorebi(config) => {
|
|
||||||
let mut messages = Vec::new();
|
|
||||||
|
|
||||||
if config.focus_monitor_at_cursor.unwrap_or(true) {
|
|
||||||
messages.push(SocketMessage::FocusMonitorAtCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.ignore_mouse_follows_focus.unwrap_or(true) {
|
|
||||||
messages.push(SocketMessage::MouseFollowsFocus(false));
|
|
||||||
messages.push(config.message.clone());
|
|
||||||
messages.push(SocketMessage::MouseFollowsFocus(mouse_follows_focus));
|
|
||||||
} else {
|
|
||||||
messages.push(config.message.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!("Sending messages: {messages:?}");
|
|
||||||
|
|
||||||
if komorebi_client::send_batch(messages.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 {
|
impl KomobarConfig {
|
||||||
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
|
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
|
||||||
let content = std::fs::read_to_string(path)?;
|
let content = std::fs::read_to_string(path)?;
|
||||||
|
|||||||
@@ -15,10 +15,12 @@ use eframe::egui::ViewportBuilder;
|
|||||||
use font_loader::system_fonts;
|
use font_loader::system_fonts;
|
||||||
use hotwatch::EventKind;
|
use hotwatch::EventKind;
|
||||||
use hotwatch::Hotwatch;
|
use hotwatch::Hotwatch;
|
||||||
|
use image::RgbaImage;
|
||||||
use komorebi_client::replace_env_in_path;
|
use komorebi_client::replace_env_in_path;
|
||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use komorebi_client::SocketMessage;
|
use komorebi_client::SocketMessage;
|
||||||
use komorebi_client::SubscribeOptions;
|
use komorebi_client::SubscribeOptions;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -26,6 +28,8 @@ use std::sync::atomic::AtomicI32;
|
|||||||
use std::sync::atomic::AtomicU32;
|
use std::sync::atomic::AtomicU32;
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::AtomicUsize;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use std::sync::Mutex;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use windows::Win32::Foundation::HWND;
|
use windows::Win32::Foundation::HWND;
|
||||||
@@ -49,6 +53,9 @@ pub static DEFAULT_PADDING: f32 = 10.0;
|
|||||||
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||||
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
|
pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
|
||||||
|
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(author, about, version)]
|
#[clap(author, about, version)]
|
||||||
struct Opts {
|
struct Opts {
|
||||||
@@ -159,7 +166,8 @@ fn main() -> color_eyre::Result<()> {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
home.is_dir(),
|
home.is_dir(),
|
||||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||||
|
home_path
|
||||||
);
|
);
|
||||||
|
|
||||||
home
|
home
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::ImageIcon;
|
use super::komorebi::img_to_texture;
|
||||||
use crate::render::RenderConfig;
|
use crate::render::RenderConfig;
|
||||||
use crate::selected_frame::SelectableFrame;
|
use crate::selected_frame::SelectableFrame;
|
||||||
use crate::widgets::widget::BarWidget;
|
use crate::widgets::widget::BarWidget;
|
||||||
@@ -17,13 +17,14 @@ use eframe::egui::Stroke;
|
|||||||
use eframe::egui::StrokeKind;
|
use eframe::egui::StrokeKind;
|
||||||
use eframe::egui::Ui;
|
use eframe::egui::Ui;
|
||||||
use eframe::egui::Vec2;
|
use eframe::egui::Vec2;
|
||||||
|
use image::DynamicImage;
|
||||||
|
use image::RgbaImage;
|
||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing;
|
use tracing;
|
||||||
@@ -118,32 +119,42 @@ impl BarWidget for Applications {
|
|||||||
|
|
||||||
impl From<&ApplicationsConfig> for Applications {
|
impl From<&ApplicationsConfig> for Applications {
|
||||||
fn from(applications_config: &ApplicationsConfig) -> Self {
|
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
|
let items = applications_config
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter_mut()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, config)| {
|
.map(|(index, app_config)| {
|
||||||
let command = UserCommand::new(&config.command);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
App {
|
App {
|
||||||
enable: config.enable.unwrap_or(applications_config.enable),
|
enable: app_config.enable.unwrap_or(applications_config.enable),
|
||||||
#[allow(clippy::obfuscated_if_else)]
|
name: app_config
|
||||||
name: config
|
|
||||||
.name
|
.name
|
||||||
.is_empty()
|
.is_empty()
|
||||||
.then(|| format!("App {}", index + 1))
|
.then(|| format!("App {}", index + 1))
|
||||||
.unwrap_or_else(|| config.name.clone()),
|
.unwrap_or_else(|| app_config.name.clone()),
|
||||||
icon: Icon::try_from_path(config.icon.as_deref())
|
icon: Icon::try_from(app_config),
|
||||||
.or_else(|| Icon::try_from_command(&command)),
|
command: app_config.command.clone(),
|
||||||
command,
|
display: app_config
|
||||||
display: config
|
|
||||||
.display
|
.display
|
||||||
.or(applications_config.display)
|
.or(applications_config.display)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
show_command_on_hover: config
|
show_command_on_hover: app_config
|
||||||
.show_command_on_hover
|
.show_command_on_hover
|
||||||
.or(applications_config.show_command_on_hover)
|
.or(applications_config.show_command_on_hover)
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
|
last_launch,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -166,11 +177,13 @@ pub struct App {
|
|||||||
/// Icon to display for this application, if available.
|
/// Icon to display for this application, if available.
|
||||||
pub icon: Option<Icon>,
|
pub icon: Option<Icon>,
|
||||||
/// Command to execute when the application is launched.
|
/// Command to execute when the application is launched.
|
||||||
pub command: UserCommand,
|
pub command: String,
|
||||||
/// Display format (icon, text, or both).
|
/// Display format (icon, text, or both).
|
||||||
pub display: DisplayFormat,
|
pub display: DisplayFormat,
|
||||||
/// Whether to show the launch command on hover.
|
/// Whether to show the launch command on hover.
|
||||||
pub show_command_on_hover: bool,
|
pub show_command_on_hover: bool,
|
||||||
|
/// Last time this application was launched (used for cooldown control).
|
||||||
|
pub last_launch: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -192,15 +205,17 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add hover text with command information
|
// Add hover text with command information
|
||||||
let response = ui.response();
|
|
||||||
if self.show_command_on_hover {
|
if self.show_command_on_hover {
|
||||||
response.on_hover_text(format!("Launch: {}", self.command.as_ref()));
|
ui.response()
|
||||||
|
.on_hover_text(format!("Launch: {}", self.command));
|
||||||
|
} else {
|
||||||
|
ui.response();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
// Launch the application when clicked
|
// Launch the application when clicked
|
||||||
self.command.launch_if_ready();
|
self.launch_if_ready();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,75 +235,84 @@ impl App {
|
|||||||
fn draw_name(&self, ui: &mut Ui) {
|
fn draw_name(&self, ui: &mut Ui) {
|
||||||
ui.add(Label::new(&self.name).selectable(false));
|
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 image/text data to be used as an icon in the UI.
|
/// Holds decoded image data to be used as an icon in the UI.
|
||||||
/// This represents source icon data before rendering.
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Icon {
|
pub enum Icon {
|
||||||
/// RGBA image used for rendering the icon.
|
/// RGBA image used for rendering the icon.
|
||||||
Image(ImageIcon),
|
Image(RgbaImage),
|
||||||
/// Text-based icon, e.g. from a font like Nerd Fonts.
|
/// Text-based icon, e.g. from a font like Nerd Fonts.
|
||||||
Text(String),
|
Text(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Icon {
|
impl Icon {
|
||||||
/// Attempts to create an [`Icon`] from a string path or text glyph/glyphs.
|
/// Attempts to create an `Icon` from the given `AppConfig`.
|
||||||
///
|
/// Loads the image from a specified icon path or extracts it from the application's
|
||||||
/// - Environment variables in the path are resolved using [`PathExt::replace_env`].
|
/// executable if the command points to a valid executable file.
|
||||||
/// - 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]
|
#[inline]
|
||||||
pub fn try_from_path(icon: Option<&str>) -> Option<Self> {
|
pub fn try_from(config: &AppConfig) -> Option<Self> {
|
||||||
let icon = icon.map(str::trim)?;
|
if let Some(icon) = config.icon.as_deref().map(str::trim) {
|
||||||
if icon.is_empty() {
|
if !icon.is_empty() {
|
||||||
return None;
|
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)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let path = icon.replace_env();
|
} else {
|
||||||
if !path.is_file() {
|
|
||||||
return Some(Icon::Text(icon.to_owned()));
|
return Some(Icon::Text(icon.to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let image_icon = ImageIcon::try_load(path.as_ref(), || match image::open(&path) {
|
|
||||||
Ok(img) => Some(img),
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("Failed to load icon from {:?}, error: {}", path, err);
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
})?;
|
|
||||||
|
|
||||||
Some(Icon::Image(image_icon))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to create an [`Icon`] by extracting an image from the executable path of a [`UserCommand`].
|
let binary = PathBuf::from(config.command.split(".exe").next()?);
|
||||||
///
|
let path = if binary.is_file() {
|
||||||
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved executable path.
|
Some(binary)
|
||||||
/// - Returns [`Icon::Image`] if an icon is successfully extracted.
|
} else {
|
||||||
/// - Returns `None` if the executable path is unavailable or icon extraction fails.
|
which(binary).ok()
|
||||||
#[inline]
|
};
|
||||||
pub fn try_from_command(command: &UserCommand) -> Option<Self> {
|
|
||||||
let path = command.get_executable()?;
|
match path {
|
||||||
let image_icon = ImageIcon::try_load(path.as_ref(), || {
|
Some(path) => windows_icons::get_icon_by_path(&path.to_string_lossy())
|
||||||
let path_str = path.to_str()?;
|
.or_else(|| windows_icons_fallback::get_icon_by_path(&path.to_string_lossy()))
|
||||||
windows_icons::get_icon_by_path(path_str)
|
.map(Icon::Image),
|
||||||
.or_else(|| windows_icons_fallback::get_icon_by_path(path_str))
|
None => None,
|
||||||
})?;
|
}
|
||||||
Some(Icon::Image(image_icon))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the icon in the given [`Ui`] using the provided [`IconConfig`].
|
/// Renders the icon in the given `Ui` context with the specified size.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||||
match self {
|
match self {
|
||||||
Icon::Image(image_icon) => {
|
Icon::Image(image) => {
|
||||||
Frame::NONE
|
Frame::NONE
|
||||||
.inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
|
.inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.add(
|
ui.add(
|
||||||
Image::from_texture(&image_icon.texture(ctx))
|
Image::from(&img_to_texture(ctx, image))
|
||||||
.maintain_aspect_ratio(true)
|
.maintain_aspect_ratio(true)
|
||||||
.fit_to_exact_size(Vec2::splat(icon_config.size)),
|
.fit_to_exact_size(Vec2::splat(icon_config.size)),
|
||||||
);
|
);
|
||||||
@@ -330,77 +354,3 @@ pub struct IconConfig {
|
|||||||
/// Color of the icon used for text-based icons
|
/// Color of the icon used for text-based icons
|
||||||
pub color: Color32,
|
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"])
|
.args(["/C", "start", "ms-settings:batterysaver"])
|
||||||
.spawn()
|
.spawn()
|
||||||
{
|
{
|
||||||
eprintln!("{error}")
|
eprintln!("{}", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ impl Cpu {
|
|||||||
|
|
||||||
CpuOutput {
|
CpuOutput {
|
||||||
label: match self.label_prefix {
|
label: match self.label_prefix {
|
||||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {used}%"),
|
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
|
||||||
LabelPrefix::None | LabelPrefix::Icon => format!("{used}%"),
|
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
|
||||||
},
|
},
|
||||||
selected,
|
selected,
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ impl BarWidget for Cpu {
|
|||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||||
{
|
{
|
||||||
eprintln!("{error}")
|
eprintln!("{}", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ impl Date {
|
|||||||
.to_string()
|
.to_string()
|
||||||
.trim()
|
.trim()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
Err(_) => format!("Invalid timezone: {timezone}"),
|
Err(_) => format!("Invalid timezone: {}", timezone),
|
||||||
},
|
},
|
||||||
None => Local::now()
|
None => Local::now()
|
||||||
.format(&self.format.fmt_string())
|
.format(&self.format.fmt_string())
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,8 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
|
|||||||
let s: String = String::deserialize(deserializer)?;
|
let s: String = String::deserialize(deserializer)?;
|
||||||
|
|
||||||
// Attempt to deserialize the string as a DefaultLayout
|
// 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));
|
return Ok(KomorebiLayout::Default(default_layout));
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
|
|||||||
"Floating" => Ok(KomorebiLayout::Floating),
|
"Floating" => Ok(KomorebiLayout::Floating),
|
||||||
"Paused" => Ok(KomorebiLayout::Paused),
|
"Paused" => Ok(KomorebiLayout::Paused),
|
||||||
"Custom" => Ok(KomorebiLayout::Custom),
|
"Custom" => Ok(KomorebiLayout::Custom),
|
||||||
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
|
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,12 +188,6 @@ impl KomorebiLayout {
|
|||||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], 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::Monocle => {}
|
||||||
KomorebiLayout::Floating => {
|
KomorebiLayout::Floating => {
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ impl Memory {
|
|||||||
MemoryOutput {
|
MemoryOutput {
|
||||||
label: match self.label_prefix {
|
label: match self.label_prefix {
|
||||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||||
format!("RAM: {usage}%")
|
format!("RAM: {}%", usage)
|
||||||
}
|
}
|
||||||
LabelPrefix::None | LabelPrefix::Icon => format!("{usage}%"),
|
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
|
||||||
},
|
},
|
||||||
selected,
|
selected,
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@ impl BarWidget for Memory {
|
|||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||||
{
|
{
|
||||||
eprintln!("{error}")
|
eprintln!("{}", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,3 @@
|
|||||||
use eframe::egui::ColorImage;
|
|
||||||
use eframe::egui::Context;
|
|
||||||
use eframe::egui::TextureHandle;
|
|
||||||
use eframe::egui::TextureOptions;
|
|
||||||
use image::RgbaImage;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
use std::sync::RwLock;
|
|
||||||
|
|
||||||
pub mod applications;
|
pub mod applications;
|
||||||
pub mod battery;
|
pub mod battery;
|
||||||
pub mod cpu;
|
pub mod cpu;
|
||||||
@@ -23,151 +12,3 @@ pub mod storage;
|
|||||||
pub mod time;
|
pub mod time;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
pub mod widget;
|
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()
|
.clicked()
|
||||||
{
|
{
|
||||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
||||||
eprintln!("{error}");
|
eprintln!("{}", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -535,6 +535,6 @@ enum DataUnit {
|
|||||||
|
|
||||||
impl fmt::Display for DataUnit {
|
impl fmt::Display for DataUnit {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{self:?}")
|
write!(f, "{:?}", self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ pub struct StorageConfig {
|
|||||||
pub data_refresh_interval: Option<u64>,
|
pub data_refresh_interval: Option<u64>,
|
||||||
/// Display label prefix
|
/// Display label prefix
|
||||||
pub label_prefix: Option<LabelPrefix>,
|
pub label_prefix: Option<LabelPrefix>,
|
||||||
/// Show disks that are read only. (default: false)
|
|
||||||
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]]
|
/// Select when the current percentage is over this value [[1-100]]
|
||||||
pub auto_select_over: Option<u8>,
|
pub auto_select_over: Option<u8>,
|
||||||
/// Hide when the current percentage is under this value [[1-100]]
|
/// Hide when the current percentage is under this value [[1-100]]
|
||||||
@@ -42,8 +38,6 @@ impl From<StorageConfig> for Storage {
|
|||||||
disks: Disks::new_with_refreshed_list(),
|
disks: Disks::new_with_refreshed_list(),
|
||||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||||
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
|
|
||||||
show_removable_disks: value.show_removable_disks.unwrap_or(true),
|
|
||||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||||
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
||||||
last_updated: Instant::now(),
|
last_updated: Instant::now(),
|
||||||
@@ -61,8 +55,6 @@ pub struct Storage {
|
|||||||
disks: Disks,
|
disks: Disks,
|
||||||
data_refresh_interval: u64,
|
data_refresh_interval: u64,
|
||||||
label_prefix: LabelPrefix,
|
label_prefix: LabelPrefix,
|
||||||
show_read_only_disks: bool,
|
|
||||||
show_removable_disks: bool,
|
|
||||||
auto_select_over: Option<u8>,
|
auto_select_over: Option<u8>,
|
||||||
auto_hide_under: Option<u8>,
|
auto_hide_under: Option<u8>,
|
||||||
last_updated: Instant,
|
last_updated: Instant,
|
||||||
@@ -79,12 +71,6 @@ impl Storage {
|
|||||||
let mut disks = vec![];
|
let mut disks = vec![];
|
||||||
|
|
||||||
for disk in &self.disks {
|
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 mount = disk.mount_point();
|
||||||
let total = disk.total_space();
|
let total = disk.total_space();
|
||||||
let available = disk.available_space();
|
let available = disk.available_space();
|
||||||
@@ -101,7 +87,7 @@ impl Storage {
|
|||||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||||
format!("{} {}%", mount.to_string_lossy(), percentage)
|
format!("{} {}%", mount.to_string_lossy(), percentage)
|
||||||
}
|
}
|
||||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
|
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
|
||||||
},
|
},
|
||||||
selected,
|
selected,
|
||||||
})
|
})
|
||||||
@@ -165,7 +151,7 @@ impl BarWidget for Storage {
|
|||||||
])
|
])
|
||||||
.spawn()
|
.spawn()
|
||||||
{
|
{
|
||||||
eprintln!("{error}")
|
eprintln!("{}", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ impl Time {
|
|||||||
Some(dt.time()),
|
Some(dt.time()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Err(_) => (format!("Invalid timezone: {timezone:?}"), None),
|
Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
let dt = Local::now();
|
let dt = Local::now();
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ impl BarWidget for Update {
|
|||||||
)])
|
)])
|
||||||
.spawn()
|
.spawn()
|
||||||
{
|
{
|
||||||
eprintln!("{error}")
|
eprintln!("{}", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-client"
|
name = "komorebi-client"
|
||||||
version = "0.1.38"
|
version = "0.1.37"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
komorebi = { path = "../komorebi", default-features = false }
|
komorebi = { path = "../komorebi" }
|
||||||
|
|
||||||
uds_windows = { workspace = true }
|
uds_windows = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["schemars"]
|
default = ["schemars"]
|
||||||
schemars = ["komorebi/default"]
|
schemars = ["komorebi/schemars"]
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ pub use komorebi::WorkspaceConfig;
|
|||||||
|
|
||||||
use komorebi::DATA_DIR;
|
use komorebi::DATA_DIR;
|
||||||
|
|
||||||
use std::borrow::Borrow;
|
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -95,15 +94,12 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
|||||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()>
|
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
|
||||||
where
|
|
||||||
Q: Borrow<SocketMessage>,
|
|
||||||
{
|
|
||||||
let socket = DATA_DIR.join(KOMOREBI);
|
let socket = DATA_DIR.join(KOMOREBI);
|
||||||
let mut stream = UnixStream::connect(socket)?;
|
let mut stream = UnixStream::connect(socket)?;
|
||||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||||
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
|
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
|
||||||
if let Ok(m_str) = serde_json::to_string(m.borrow()) {
|
if let Ok(m_str) = serde_json::to_string(&m) {
|
||||||
s.push_str(&m_str);
|
s.push_str(&m_str);
|
||||||
s.push('\n');
|
s.push('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-gui"
|
name = "komorebi-gui"
|
||||||
version = "0.1.38"
|
version = "0.1.37"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
komorebi-client = { path = "../komorebi-client" }
|
||||||
|
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
egui_extras = { workspace = true }
|
egui_extras = { workspace = true }
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.9" }
|
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "29df24ff2dd715655b0366bd2a598837c699a8e9" }
|
||||||
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.9" }
|
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "29df24ff2dd715655b0366bd2a598837c699a8e9" }
|
||||||
|
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use eframe::egui::ViewportBuilder;
|
use eframe::egui::ViewportBuilder;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use whkd_core::HotkeyBinding;
|
||||||
use whkd_core::Whkdrc;
|
use whkd_core::Whkdrc;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -57,14 +58,16 @@ impl eframe::App for Quicklook {
|
|||||||
ui.label("Filter");
|
ui.label("Filter");
|
||||||
ui.add(
|
ui.add(
|
||||||
eframe::egui::text_edit::TextEdit::singleline(&mut self.filter)
|
eframe::egui::text_edit::TextEdit::singleline(&mut self.filter)
|
||||||
.hint_text("Filter by command...")
|
|
||||||
.background_color(ctx.style().visuals.faint_bg_color),
|
.background_color(ctx.style().visuals.faint_bg_color),
|
||||||
);
|
);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
for binding in &whkdrc.bindings {
|
for binding in &whkdrc.bindings {
|
||||||
|
if is_komorebic_binding(binding) {
|
||||||
let keys = binding.keys.join(" + ");
|
let keys = binding.keys.join(" + ");
|
||||||
if self.filter.is_empty() || binding.command.contains(&self.filter)
|
if self.filter.is_empty()
|
||||||
|
|| binding.command.contains(&self.filter)
|
||||||
{
|
{
|
||||||
ui.label(keys);
|
ui.label(keys);
|
||||||
ui.label(&binding.command);
|
ui.label(&binding.command);
|
||||||
@@ -72,6 +75,7 @@ impl eframe::App for Quicklook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -96,3 +100,7 @@ fn main() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_komorebic_binding(binding: &HotkeyBinding) -> bool {
|
||||||
|
binding.command.starts_with("komorebic")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-themes"
|
name = "komorebi-themes"
|
||||||
version = "0.1.38"
|
version = "0.1.37"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi"
|
name = "komorebi"
|
||||||
version = "0.1.38"
|
version = "0.1.37"
|
||||||
description = "A tiling window manager for Windows"
|
description = "A tiling window manager for Windows"
|
||||||
repository = "https://github.com/LGUG2Z/komorebi"
|
repository = "https://github.com/LGUG2Z/komorebi"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -17,6 +17,7 @@ crossbeam-channel = { workspace = true }
|
|||||||
crossbeam-utils = { workspace = true }
|
crossbeam-utils = { workspace = true }
|
||||||
ctrlc = { version = "3", features = ["termination"] }
|
ctrlc = { version = "3", features = ["termination"] }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
|
dunce = { workspace = true }
|
||||||
getset = "0.1"
|
getset = "0.1"
|
||||||
hotwatch = { workspace = true }
|
hotwatch = { workspace = true }
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
@@ -24,7 +25,7 @@ miow = "0.6"
|
|||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
net2 = "0.2"
|
net2 = "0.2"
|
||||||
os_info = "3.10"
|
os_info = "3.10"
|
||||||
parking_lot = { workspace = true }
|
parking_lot = "0.12"
|
||||||
paste = { workspace = true }
|
paste = { workspace = true }
|
||||||
powershell_script = "1.0"
|
powershell_script = "1.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
|||||||
@@ -17,5 +17,5 @@ pub enum AnimationPrefix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
|
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}");
|
tracing::error!("failed to update border position {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect) {
|
if !rect.is_same_size_as(&old_rect) {
|
||||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||||
let border_width = (*border_pointer).width;
|
let border_width = (*border_pointer).width;
|
||||||
let border_offset = (*border_pointer).offset;
|
let border_offset = (*border_pointer).offset;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use crate::core::BorderStyle;
|
|||||||
use crate::core::WindowKind;
|
use crate::core::WindowKind;
|
||||||
use crate::ring::Ring;
|
use crate::ring::Ring;
|
||||||
use crate::windows_api;
|
use crate::windows_api;
|
||||||
use crate::workspace::Workspace;
|
|
||||||
use crate::workspace::WorkspaceLayer;
|
use crate::workspace::WorkspaceLayer;
|
||||||
use crate::WindowManager;
|
use crate::WindowManager;
|
||||||
use crate::WindowsApi;
|
use crate::WindowsApi;
|
||||||
@@ -213,8 +212,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
[focused_workspace_idx]
|
[focused_workspace_idx]
|
||||||
.layer();
|
.layer();
|
||||||
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
|
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
|
||||||
let layer_changed = previous_layer != workspace_layer;
|
|
||||||
let forced_update = matches!(notification, Notification::ForceUpdate);
|
|
||||||
|
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
@@ -237,17 +234,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.set_accent(window_kind_colour(window_kind))?;
|
.set_accent(window_kind_colour(window_kind))?;
|
||||||
|
|
||||||
if ws.layer() == &WorkspaceLayer::Floating {
|
|
||||||
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;
|
continue 'monitors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +241,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
let window_kind = if idx != ws.focused_container_idx()
|
let window_kind = if idx != ws.focused_container_idx()
|
||||||
|| monitor_idx != focused_monitor_idx
|
|| monitor_idx != focused_monitor_idx
|
||||||
{
|
{
|
||||||
if c.locked() {
|
if ws.locked_containers().contains(&idx) {
|
||||||
WindowKind::UnfocusedLocked
|
WindowKind::UnfocusedLocked
|
||||||
} else {
|
} else {
|
||||||
WindowKind::Unfocused
|
WindowKind::Unfocused
|
||||||
@@ -356,7 +342,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !should_process_notification {
|
if !should_process_notification {
|
||||||
tracing::debug!("monitor state matches latest snapshot, skipping notification");
|
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||||
continue 'receiver;
|
continue 'receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,32 +448,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
windows_borders.insert(focused_window_hwnd, id);
|
windows_borders.insert(focused_window_hwnd, id);
|
||||||
|
|
||||||
let border_hwnd = border.hwnd;
|
let border_hwnd = border.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 all borders on this monitor except monocle
|
||||||
remove_borders(
|
remove_borders(
|
||||||
&mut borders,
|
&mut borders,
|
||||||
@@ -495,7 +455,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
monitor_idx,
|
monitor_idx,
|
||||||
|_, b| border_hwnd != b.hwnd,
|
|_, b| border_hwnd != b.hwnd,
|
||||||
)?;
|
)?;
|
||||||
}
|
|
||||||
continue 'monitors;
|
continue 'monitors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,7 +523,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
|| monitor_idx != focused_monitor_idx
|
|| monitor_idx != focused_monitor_idx
|
||||||
|| focused_window_hwnd != foreground_window
|
|| focused_window_hwnd != foreground_window
|
||||||
{
|
{
|
||||||
if c.locked() {
|
if ws.locked_containers().contains(&idx) {
|
||||||
WindowKind::UnfocusedLocked
|
WindowKind::UnfocusedLocked
|
||||||
} else {
|
} else {
|
||||||
WindowKind::Unfocused
|
WindowKind::Unfocused
|
||||||
@@ -609,6 +569,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
};
|
};
|
||||||
border.window_rect = rect;
|
border.window_rect = rect;
|
||||||
|
|
||||||
|
let layer_changed = previous_layer != workspace_layer;
|
||||||
|
let forced_update = matches!(notification, Notification::ForceUpdate);
|
||||||
|
|
||||||
let should_invalidate = new_border
|
let should_invalidate = new_border
|
||||||
|| (last_focus_state != new_focus_state)
|
|| (last_focus_state != new_focus_state)
|
||||||
|| layer_changed
|
|| layer_changed
|
||||||
@@ -628,52 +591,22 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
|||||||
windows_borders.insert(focused_window_hwnd, id);
|
windows_borders.insert(focused_window_hwnd, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_floating_borders(
|
{
|
||||||
&mut borders,
|
|
||||||
&mut windows_borders,
|
|
||||||
ws,
|
|
||||||
monitor_idx,
|
|
||||||
foreground_window,
|
|
||||||
layer_changed,
|
|
||||||
forced_update,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
previous_snapshot = monitors;
|
|
||||||
previous_pending_move_op = pending_move_op;
|
|
||||||
previous_is_paused = is_paused;
|
|
||||||
previous_notification = Some(notification);
|
|
||||||
previous_layer = workspace_layer;
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
for window in ws.floating_windows() {
|
||||||
let mut new_border = false;
|
let mut new_border = false;
|
||||||
let id = window.hwnd.to_string();
|
let id = window.hwnd.to_string();
|
||||||
let border = match borders.entry(id.clone()) {
|
let border = match borders.entry(id.clone()) {
|
||||||
Entry::Occupied(entry) => entry.into_mut(),
|
Entry::Occupied(entry) => entry.into_mut(),
|
||||||
Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
if let Ok(border) =
|
if let Ok(border) = Border::create(
|
||||||
Border::create(&window.hwnd.to_string(), window.hwnd, monitor_idx)
|
&window.hwnd.to_string(),
|
||||||
{
|
window.hwnd,
|
||||||
|
monitor_idx,
|
||||||
|
) {
|
||||||
new_border = true;
|
new_border = true;
|
||||||
entry.insert(border)
|
entry.insert(border)
|
||||||
} else {
|
} else {
|
||||||
return Ok(());
|
continue 'monitors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -694,8 +627,14 @@ fn handle_floating_borders(
|
|||||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||||
border.window_rect = rect;
|
border.window_rect = rect;
|
||||||
|
|
||||||
let should_invalidate =
|
let layer_changed = previous_layer != workspace_layer;
|
||||||
new_border || (last_focus_state != new_focus_state) || layer_changed || forced_update;
|
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 should_invalidate {
|
||||||
if forced_update && !new_border {
|
if forced_update && !new_border {
|
||||||
@@ -710,6 +649,18 @@ fn handle_floating_borders(
|
|||||||
|
|
||||||
windows_borders.insert(window.hwnd, id);
|
windows_borders.insert(window.hwnd, id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previous_snapshot = monitors;
|
||||||
|
previous_pending_move_op = pending_move_op;
|
||||||
|
previous_is_paused = is_paused;
|
||||||
|
previous_notification = Some(notification);
|
||||||
|
previous_layer = workspace_layer;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use getset::CopyGetters;
|
|
||||||
use getset::Getters;
|
use getset::Getters;
|
||||||
use getset::Setters;
|
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::ring::Ring;
|
use crate::ring::Ring;
|
||||||
use crate::window::Window;
|
use crate::window::Window;
|
||||||
use crate::Lockable;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, CopyGetters, Setters)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
#[getset(get = "pub")]
|
#[getset(get = "pub")]
|
||||||
id: String,
|
id: String,
|
||||||
#[serde(default)]
|
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
|
||||||
locked: bool,
|
|
||||||
windows: Ring<Window>,
|
windows: Ring<Window>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,23 +22,11 @@ impl Default for Container {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: nanoid!(),
|
id: nanoid!(),
|
||||||
locked: false,
|
|
||||||
windows: Ring::default(),
|
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 {
|
impl Container {
|
||||||
pub fn hide(&self, omit: Option<isize>) {
|
pub fn hide(&self, omit: Option<isize>) {
|
||||||
for window in self.windows().iter().rev() {
|
for window in self.windows().iter().rev() {
|
||||||
@@ -162,7 +144,6 @@ impl Container {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_contains_window() {
|
fn test_contains_window() {
|
||||||
@@ -269,40 +250,4 @@ mod tests {
|
|||||||
// Should return None since window 4 doesn't exist
|
// Should return None since window 4 doesn't exist
|
||||||
assert_eq!(container.idx_for_window(4), None);
|
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,10 +12,8 @@ use super::custom_layout::ColumnSplitWithCapacity;
|
|||||||
use super::CustomLayout;
|
use super::CustomLayout;
|
||||||
use super::DefaultLayout;
|
use super::DefaultLayout;
|
||||||
use super::Rect;
|
use super::Rect;
|
||||||
use crate::default_layout::LayoutOptions;
|
|
||||||
|
|
||||||
pub trait Arrangement {
|
pub trait Arrangement {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn calculate(
|
fn calculate(
|
||||||
&self,
|
&self,
|
||||||
area: &Rect,
|
area: &Rect,
|
||||||
@@ -23,9 +21,6 @@ pub trait Arrangement {
|
|||||||
container_padding: Option<i32>,
|
container_padding: Option<i32>,
|
||||||
layout_flip: Option<Axis>,
|
layout_flip: Option<Axis>,
|
||||||
resize_dimensions: &[Option<Rect>],
|
resize_dimensions: &[Option<Rect>],
|
||||||
focused_idx: usize,
|
|
||||||
layout_options: Option<LayoutOptions>,
|
|
||||||
latest_layout: &[Rect],
|
|
||||||
) -> Vec<Rect>;
|
) -> Vec<Rect>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,110 +33,9 @@ impl Arrangement for DefaultLayout {
|
|||||||
container_padding: Option<i32>,
|
container_padding: Option<i32>,
|
||||||
layout_flip: Option<Axis>,
|
layout_flip: Option<Axis>,
|
||||||
resize_dimensions: &[Option<Rect>],
|
resize_dimensions: &[Option<Rect>],
|
||||||
focused_idx: usize,
|
|
||||||
layout_options: Option<LayoutOptions>,
|
|
||||||
latest_layout: &[Rect],
|
|
||||||
) -> Vec<Rect> {
|
) -> Vec<Rect> {
|
||||||
let len = usize::from(len);
|
let len = usize::from(len);
|
||||||
let mut dimensions = match self {
|
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(
|
Self::BSP => recursive_fibonacci(
|
||||||
0,
|
0,
|
||||||
len,
|
len,
|
||||||
@@ -593,9 +487,6 @@ impl Arrangement for CustomLayout {
|
|||||||
container_padding: Option<i32>,
|
container_padding: Option<i32>,
|
||||||
_layout_flip: Option<Axis>,
|
_layout_flip: Option<Axis>,
|
||||||
_resize_dimensions: &[Option<Rect>],
|
_resize_dimensions: &[Option<Rect>],
|
||||||
_focused_idx: usize,
|
|
||||||
_layout_options: Option<LayoutOptions>,
|
|
||||||
_latest_layout: &[Rect],
|
|
||||||
) -> Vec<Rect> {
|
) -> Vec<Rect> {
|
||||||
let mut dimensions = vec![];
|
let mut dimensions = vec![];
|
||||||
let container_count = len.get();
|
let container_count = len.get();
|
||||||
@@ -650,7 +541,7 @@ impl Arrangement for CustomLayout {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match column {
|
match column {
|
||||||
Column::Primary(Some(_)) => {
|
Column::Primary(Option::Some(_)) => {
|
||||||
let main_column_area = if idx == 0 {
|
let main_column_area = if idx == 0 {
|
||||||
Self::main_column_area(area, primary_right, None)
|
Self::main_column_area(area, primary_right, None)
|
||||||
} else {
|
} else {
|
||||||
@@ -1224,37 +1115,6 @@ fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rec
|
|||||||
result
|
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) {
|
fn resize_left(rect: &mut Rect, resize: i32) {
|
||||||
rect.left += resize / 2;
|
rect.left += resize / 2;
|
||||||
rect.right += -resize / 2;
|
rect.right += -resize / 2;
|
||||||
|
|||||||
@@ -21,24 +21,9 @@ pub enum DefaultLayout {
|
|||||||
UltrawideVerticalStack,
|
UltrawideVerticalStack,
|
||||||
Grid,
|
Grid,
|
||||||
RightMainVerticalStack,
|
RightMainVerticalStack,
|
||||||
Scrolling,
|
|
||||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
// 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 {
|
impl DefaultLayout {
|
||||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||||
match self {
|
match self {
|
||||||
@@ -46,7 +31,6 @@ impl DefaultLayout {
|
|||||||
n if n > 1 => 1,
|
n if n > 1 => 1,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
},
|
},
|
||||||
Self::Scrolling => 0,
|
|
||||||
DefaultLayout::BSP
|
DefaultLayout::BSP
|
||||||
| DefaultLayout::Columns
|
| DefaultLayout::Columns
|
||||||
| DefaultLayout::Rows
|
| DefaultLayout::Rows
|
||||||
@@ -69,7 +53,6 @@ impl DefaultLayout {
|
|||||||
_ => len.saturating_sub(1),
|
_ => len.saturating_sub(1),
|
||||||
},
|
},
|
||||||
DefaultLayout::RightMainVerticalStack => 0,
|
DefaultLayout::RightMainVerticalStack => 0,
|
||||||
DefaultLayout::Scrolling => len.saturating_sub(1),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +75,6 @@ impl DefaultLayout {
|
|||||||
| Self::RightMainVerticalStack
|
| Self::RightMainVerticalStack
|
||||||
| Self::HorizontalStack
|
| Self::HorizontalStack
|
||||||
| Self::UltrawideVerticalStack
|
| Self::UltrawideVerticalStack
|
||||||
| Self::Scrolling
|
|
||||||
) {
|
) {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
@@ -187,15 +169,13 @@ impl DefaultLayout {
|
|||||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||||
Self::UltrawideVerticalStack => Self::Grid,
|
Self::UltrawideVerticalStack => Self::Grid,
|
||||||
Self::Grid => Self::RightMainVerticalStack,
|
Self::Grid => Self::RightMainVerticalStack,
|
||||||
Self::RightMainVerticalStack => Self::Scrolling,
|
Self::RightMainVerticalStack => Self::BSP,
|
||||||
Self::Scrolling => Self::BSP,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn cycle_previous(self) -> Self {
|
pub const fn cycle_previous(self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Scrolling => Self::RightMainVerticalStack,
|
|
||||||
Self::RightMainVerticalStack => Self::Grid,
|
Self::RightMainVerticalStack => Self::Grid,
|
||||||
Self::Grid => Self::UltrawideVerticalStack,
|
Self::Grid => Self::UltrawideVerticalStack,
|
||||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ impl Direction for DefaultLayout {
|
|||||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
||||||
Self::UltrawideVerticalStack => idx > 2,
|
Self::UltrawideVerticalStack => idx > 2,
|
||||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||||
Self::Scrolling => false,
|
|
||||||
},
|
},
|
||||||
OperationDirection::Down => match self {
|
OperationDirection::Down => match self {
|
||||||
Self::BSP => idx != count - 1 && idx % 2 != 0,
|
Self::BSP => idx != count - 1 && idx % 2 != 0,
|
||||||
@@ -112,7 +111,6 @@ impl Direction for DefaultLayout {
|
|||||||
Self::HorizontalStack => idx == 0,
|
Self::HorizontalStack => idx == 0,
|
||||||
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||||
Self::Scrolling => false,
|
|
||||||
},
|
},
|
||||||
OperationDirection::Left => match self {
|
OperationDirection::Left => match self {
|
||||||
Self::BSP => idx != 0,
|
Self::BSP => idx != 0,
|
||||||
@@ -122,7 +120,6 @@ impl Direction for DefaultLayout {
|
|||||||
Self::HorizontalStack => idx != 0 && idx != 1,
|
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||||
Self::UltrawideVerticalStack => idx != 1,
|
Self::UltrawideVerticalStack => idx != 1,
|
||||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||||
Self::Scrolling => idx != 0,
|
|
||||||
},
|
},
|
||||||
OperationDirection::Right => match self {
|
OperationDirection::Right => match self {
|
||||||
Self::BSP => idx % 2 == 0 && idx != count - 1,
|
Self::BSP => idx % 2 == 0 && idx != count - 1,
|
||||||
@@ -136,7 +133,6 @@ impl Direction for DefaultLayout {
|
|||||||
_ => idx < 2,
|
_ => idx < 2,
|
||||||
},
|
},
|
||||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||||
Self::Scrolling => idx != count - 1,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +158,6 @@ impl Direction for DefaultLayout {
|
|||||||
| Self::RightMainVerticalStack => idx - 1,
|
| Self::RightMainVerticalStack => idx - 1,
|
||||||
Self::HorizontalStack => 0,
|
Self::HorizontalStack => 0,
|
||||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||||
Self::Scrolling => unreachable!(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +176,6 @@ impl Direction for DefaultLayout {
|
|||||||
Self::Columns => unreachable!(),
|
Self::Columns => unreachable!(),
|
||||||
Self::HorizontalStack => 1,
|
Self::HorizontalStack => 1,
|
||||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||||
Self::Scrolling => unreachable!(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +203,6 @@ impl Direction for DefaultLayout {
|
|||||||
_ => 0,
|
_ => 0,
|
||||||
},
|
},
|
||||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||||
Self::Scrolling => idx - 1,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +223,6 @@ impl Direction for DefaultLayout {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
},
|
},
|
||||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||||
Self::Scrolling => idx + 1,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#![warn(clippy::all)]
|
#![warn(clippy::all)]
|
||||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||||
|
|
||||||
use std::num::NonZeroUsize;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
@@ -109,7 +108,6 @@ pub enum SocketMessage {
|
|||||||
AdjustWorkspacePadding(Sizing, i32),
|
AdjustWorkspacePadding(Sizing, i32),
|
||||||
ChangeLayout(DefaultLayout),
|
ChangeLayout(DefaultLayout),
|
||||||
CycleLayout(CycleDirection),
|
CycleLayout(CycleDirection),
|
||||||
ScrollingLayoutColumns(NonZeroUsize),
|
|
||||||
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||||
FlipLayout(Axis),
|
FlipLayout(Axis),
|
||||||
ToggleWorkspaceWindowContainerBehaviour,
|
ToggleWorkspaceWindowContainerBehaviour,
|
||||||
@@ -202,7 +200,6 @@ pub enum SocketMessage {
|
|||||||
StackbarFontFamily(Option<String>),
|
StackbarFontFamily(Option<String>),
|
||||||
WorkAreaOffset(Rect),
|
WorkAreaOffset(Rect),
|
||||||
MonitorWorkAreaOffset(usize, Rect),
|
MonitorWorkAreaOffset(usize, Rect),
|
||||||
WorkspaceWorkAreaOffset(usize, usize, Rect),
|
|
||||||
ToggleWindowBasedWorkAreaOffset,
|
ToggleWindowBasedWorkAreaOffset,
|
||||||
ResizeDelta(i32),
|
ResizeDelta(i32),
|
||||||
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||||
@@ -344,7 +341,6 @@ pub enum StateQuery {
|
|||||||
FocusedWindowIndex,
|
FocusedWindowIndex,
|
||||||
FocusedWorkspaceName,
|
FocusedWorkspaceName,
|
||||||
FocusedWorkspaceLayout,
|
FocusedWorkspaceLayout,
|
||||||
FocusedContainerKind,
|
|
||||||
Version,
|
Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ use serde::Serialize;
|
|||||||
|
|
||||||
/// Path extension trait
|
/// Path extension trait
|
||||||
pub trait PathExt {
|
pub trait PathExt {
|
||||||
/// Resolve environment variable components in a path.
|
/// Resolve environment variables components in a path.
|
||||||
///
|
///
|
||||||
/// Resolves the following formats:
|
/// Resolves the follwing formats:
|
||||||
/// - CMD: `%variable%`
|
/// - CMD: `%variable%`
|
||||||
/// - PowerShell: `$Env:variable`
|
/// - PowerShell: `$Env:variable`
|
||||||
/// - Bash: `$variable`.
|
/// - Bash: `$variable`.
|
||||||
|
|||||||
@@ -41,10 +41,6 @@ impl Rect {
|
|||||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||||
self.right == rhs.right && self.bottom == rhs.bottom
|
self.right == rhs.right && self.bottom == rhs.bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_same_position_as(&self, rhs: &Self) -> bool {
|
|
||||||
self.left == rhs.left && self.top == rhs.top
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rect {
|
impl Rect {
|
||||||
|
|||||||
+5
-12
@@ -8,7 +8,7 @@ pub mod ring;
|
|||||||
pub mod container;
|
pub mod container;
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod focus_manager;
|
pub mod focus_manager;
|
||||||
pub mod lockable_sequence;
|
pub mod locked_deque;
|
||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
pub mod monitor_reconciliator;
|
pub mod monitor_reconciliator;
|
||||||
pub mod process_command;
|
pub mod process_command;
|
||||||
@@ -200,7 +200,8 @@ lazy_static! {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
home.is_dir(),
|
home.is_dir(),
|
||||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||||
|
home_path
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -254,14 +255,6 @@ pub static WINDOW_HANDLING_BEHAVIOUR: AtomicCell<WindowHandlingBehaviour> =
|
|||||||
|
|
||||||
shadow_rs::shadow!(build);
|
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]
|
#[must_use]
|
||||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
@@ -306,7 +299,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
|||||||
current
|
current
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum NotificationEvent {
|
pub enum NotificationEvent {
|
||||||
@@ -323,7 +316,7 @@ pub enum VirtualDesktopNotification {
|
|||||||
LeftAssociatedVirtualDesktop,
|
LeftAssociatedVirtualDesktop,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct Notification {
|
pub struct Notification {
|
||||||
pub event: NotificationEvent,
|
pub event: NotificationEvent,
|
||||||
|
|||||||
@@ -1,357 +0,0 @@
|
|||||||
use std::collections::VecDeque;
|
|
||||||
|
|
||||||
use crate::Lockable;
|
|
||||||
|
|
||||||
/// A sequence supporting insertion, removal, and swapping of elements while preserving the absolute
|
|
||||||
/// positions of locked items.
|
|
||||||
pub trait LockableSequence<T: Lockable> {
|
|
||||||
/// Inserts a value at `idx`, keeping locked elements at their absolute positions.
|
|
||||||
fn insert_respecting_locks(&mut self, idx: usize, value: T) -> usize;
|
|
||||||
/// Removes the element at `idx`, keeping locked elements at their absolute positions.
|
|
||||||
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T>;
|
|
||||||
/// Swaps the elements at indices `i` and `j`, keeping locked elements at their absolute positions.
|
|
||||||
fn swap_respecting_locks(&mut self, i: usize, j: usize);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Lockable> LockableSequence<T> for VecDeque<T> {
|
|
||||||
/// Insert `value` at logical index `idx`, with trying to keep locked elements
|
|
||||||
/// (`is_locked()`) anchored at their original positions.
|
|
||||||
///
|
|
||||||
/// Returns the final index of the inserted element.
|
|
||||||
fn insert_respecting_locks(&mut self, mut idx: usize, value: T) -> usize {
|
|
||||||
// 1. Bounds check: if index is out of range, simply append.
|
|
||||||
if idx >= self.len() {
|
|
||||||
self.push_back(value);
|
|
||||||
return self.len() - 1; // last index
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Normal VecDeque insertion
|
|
||||||
self.insert(idx, value);
|
|
||||||
|
|
||||||
// 3. Walk left-to-right once, swapping any misplaced locked element. After
|
|
||||||
// the VecDeque::insert all items after `idx` have moved right by one. For every locked
|
|
||||||
// element that is now to the right of an unlocked one, swap it back left exactly once.
|
|
||||||
for index in (idx + 1)..self.len() {
|
|
||||||
if self[index].locked() && !self[index - 1].locked() {
|
|
||||||
self.swap(index - 1, index);
|
|
||||||
|
|
||||||
// If the element we just inserted participated in the swap,
|
|
||||||
// update `idx` so we can return its final location.
|
|
||||||
if idx == index - 1 {
|
|
||||||
idx = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
idx
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove element at `idx`, with trying to keep locked elements
|
|
||||||
/// (`is_locked()`) anchored at their original positions.
|
|
||||||
///
|
|
||||||
/// Returns the removed element, or `None` if `idx` is out of bounds.
|
|
||||||
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T> {
|
|
||||||
// 1. Bounds check: if index is out of range, do nothing.
|
|
||||||
if idx >= self.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Remove the element at the requested index.
|
|
||||||
// All elements after idx are now shifted left by 1.
|
|
||||||
let removed = self.remove(idx)?;
|
|
||||||
|
|
||||||
// 3. If less than 2 elements remain, nothing to shift.
|
|
||||||
if self.len() < 2 {
|
|
||||||
return Some(removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Iterate from the element just after the removed spot up to the second-to-last
|
|
||||||
// element, right-to-left. This loop "fixes" locked elements that were shifted left
|
|
||||||
// off their anchored positions: If a locked element now has an unlocked element
|
|
||||||
// to its right, swap them back to restore locked order.
|
|
||||||
for index in (idx..self.len() - 1).rev() {
|
|
||||||
// If current is locked and the next one is not locked, swap them.
|
|
||||||
if self[index].locked() && !self[index + 1].locked() {
|
|
||||||
self.swap(index, index + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Return the removed value.
|
|
||||||
Some(removed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Swaps the elements at indices `i` and `j`, along with their `locked` status, ensuring
|
|
||||||
/// the lock state remains associated with the position rather than the element itself.
|
|
||||||
fn swap_respecting_locks(&mut self, i: usize, j: usize) {
|
|
||||||
self.swap(i, j);
|
|
||||||
let locked_i = self[i].locked();
|
|
||||||
let locked_j = self[j].locked();
|
|
||||||
self[i].set_locked(locked_j);
|
|
||||||
self[j].set_locked(locked_i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
struct TestItem {
|
|
||||||
val: i32,
|
|
||||||
locked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Lockable for TestItem {
|
|
||||||
fn locked(&self) -> bool {
|
|
||||||
self.locked
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_locked(&mut self, locked: bool) -> &mut Self {
|
|
||||||
self.locked = locked;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn vals(v: &VecDeque<TestItem>) -> Vec<i32> {
|
|
||||||
v.iter().map(|x| x.val).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_deque(items: &[(i32, bool)]) -> VecDeque<TestItem> {
|
|
||||||
items
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|(val, locked)| TestItem { val, locked })
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_insert_respecting_locks() {
|
|
||||||
// Test case 1: Basic insertion with locked index
|
|
||||||
{
|
|
||||||
// Lock index 2
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
|
||||||
// Insert at index 0, should shift elements while keeping index 2 locked
|
|
||||||
ring.insert_respecting_locks(
|
|
||||||
0,
|
|
||||||
TestItem {
|
|
||||||
val: 99,
|
|
||||||
locked: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
|
|
||||||
assert_eq!(vals(&ring), vec![99, 0, 2, 1, 3, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 2: Insert at a locked index (should insert after locked)
|
|
||||||
{
|
|
||||||
// Lock index 2
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
|
||||||
// Try to insert at locked index 2, should insert at index 3 instead
|
|
||||||
let actual_index = ring.insert_respecting_locks(
|
|
||||||
2,
|
|
||||||
TestItem {
|
|
||||||
val: 99,
|
|
||||||
locked: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
assert_eq!(actual_index, 3);
|
|
||||||
assert_eq!(vals(&ring), vec![0, 1, 2, 99, 3, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 3: Multiple locked indices
|
|
||||||
{
|
|
||||||
// Lock index 1 and 3
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
|
|
||||||
// Insert at index 0, should maintain locked indices
|
|
||||||
ring.insert_respecting_locks(
|
|
||||||
0,
|
|
||||||
TestItem {
|
|
||||||
val: 99,
|
|
||||||
locked: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// Elements '1' and '3' remain at indices 1 and 3
|
|
||||||
assert_eq!(vals(&ring), vec![99, 1, 0, 3, 2, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 4: Insert at end
|
|
||||||
{
|
|
||||||
// Lock index 2
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
|
||||||
let actual_index = ring.insert_respecting_locks(
|
|
||||||
5,
|
|
||||||
TestItem {
|
|
||||||
val: 99,
|
|
||||||
locked: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
assert_eq!(actual_index, 5);
|
|
||||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 5: Empty ring
|
|
||||||
{
|
|
||||||
let mut ring = test_deque(&[]);
|
|
||||||
// Insert into empty deque
|
|
||||||
let actual_index = ring.insert_respecting_locks(
|
|
||||||
0,
|
|
||||||
TestItem {
|
|
||||||
val: 99,
|
|
||||||
locked: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
assert_eq!(actual_index, 0);
|
|
||||||
assert_eq!(vals(&ring), vec![99]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 6: All indices locked
|
|
||||||
{
|
|
||||||
// Lock all indices
|
|
||||||
let mut ring = test_deque(&[(0, true), (1, true), (2, true), (3, true), (4, true)]);
|
|
||||||
// Try to insert at index 2, should insert at the end
|
|
||||||
let actual_index = ring.insert_respecting_locks(
|
|
||||||
2,
|
|
||||||
TestItem {
|
|
||||||
val: 99,
|
|
||||||
locked: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
assert_eq!(actual_index, 5);
|
|
||||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 7: Consecutive locked indices
|
|
||||||
{
|
|
||||||
// Lock index 2 and 3
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, true), (4, false)]);
|
|
||||||
// Insert at index 1, should maintain consecutive locked indices
|
|
||||||
ring.insert_respecting_locks(
|
|
||||||
1,
|
|
||||||
TestItem {
|
|
||||||
val: 99,
|
|
||||||
locked: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// Elements '2' and '3' remain at indices 2 and 3
|
|
||||||
assert_eq!(vals(&ring), vec![0, 99, 2, 3, 1, 4]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_respecting_locks() {
|
|
||||||
// Test case 1: Remove a non-locked index before a locked index
|
|
||||||
{
|
|
||||||
// Lock index 2
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
|
||||||
let removed = ring.remove_respecting_locks(0);
|
|
||||||
assert_eq!(removed.map(|x| x.val), Some(0));
|
|
||||||
// Elements '2' remain at index 2
|
|
||||||
assert_eq!(vals(&ring), vec![1, 3, 2, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 2: Remove a locked index
|
|
||||||
{
|
|
||||||
// Lock index 2
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
|
||||||
let removed = ring.remove_respecting_locks(2);
|
|
||||||
assert_eq!(removed.map(|x| x.val), Some(2));
|
|
||||||
// Elements should stay at the same places
|
|
||||||
assert_eq!(vals(&ring), vec![0, 1, 3, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 3: Remove an index after a locked index
|
|
||||||
{
|
|
||||||
// Lock index 1
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false), (4, false)]);
|
|
||||||
let removed = ring.remove_respecting_locks(3);
|
|
||||||
assert_eq!(removed.map(|x| x.val), Some(3));
|
|
||||||
// Elements should stay at the same places
|
|
||||||
assert_eq!(vals(&ring), vec![0, 1, 2, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 4: Multiple locked indices
|
|
||||||
{
|
|
||||||
// Lock index 1 and 3
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
|
|
||||||
let removed = ring.remove_respecting_locks(0);
|
|
||||||
assert_eq!(removed.map(|x| x.val), Some(0));
|
|
||||||
// Elements '1' and '3' remain at indices '1' and '3'
|
|
||||||
assert_eq!(vals(&ring), vec![2, 1, 4, 3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 5: Remove the last element
|
|
||||||
{
|
|
||||||
// Lock index 2
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
|
||||||
let removed = ring.remove_respecting_locks(4);
|
|
||||||
assert_eq!(removed.map(|x| x.val), Some(4));
|
|
||||||
// Index 2 should still be at the same place
|
|
||||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 6: Invalid index
|
|
||||||
{
|
|
||||||
// Lock index 2
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
|
||||||
let removed = ring.remove_respecting_locks(10);
|
|
||||||
assert_eq!(removed, None);
|
|
||||||
// Deque unchanged
|
|
||||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 7: Remove enough elements to make a locked index invalid
|
|
||||||
{
|
|
||||||
// Lock index 2
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true)]);
|
|
||||||
ring.remove_respecting_locks(0);
|
|
||||||
// Index 2 should now be '1'
|
|
||||||
assert_eq!(vals(&ring), vec![1, 2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test case 8: Removing an element before multiple locked indices
|
|
||||||
{
|
|
||||||
// Lock index 2 and 4
|
|
||||||
let mut ring = test_deque(&[
|
|
||||||
(0, false),
|
|
||||||
(1, false),
|
|
||||||
(2, true),
|
|
||||||
(3, false),
|
|
||||||
(4, true),
|
|
||||||
(5, false),
|
|
||||||
]);
|
|
||||||
let removed = ring.remove_respecting_locks(1);
|
|
||||||
assert_eq!(removed.map(|x| x.val), Some(1));
|
|
||||||
// Both indices should still be at the same place
|
|
||||||
assert_eq!(vals(&ring), vec![0, 3, 2, 5, 4]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_swap_respecting_locks_various_cases() {
|
|
||||||
// Swap unlocked and locked
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false)]);
|
|
||||||
ring.swap_respecting_locks(0, 1);
|
|
||||||
assert_eq!(vals(&ring), vec![1, 0, 2, 3]);
|
|
||||||
assert!(!ring[0].locked);
|
|
||||||
assert!(ring[1].locked);
|
|
||||||
ring.swap_respecting_locks(0, 1);
|
|
||||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
|
|
||||||
assert!(!ring[0].locked);
|
|
||||||
assert!(ring[1].locked);
|
|
||||||
|
|
||||||
// Both locked
|
|
||||||
let mut ring = test_deque(&[(0, true), (1, false), (2, true)]);
|
|
||||||
ring.swap_respecting_locks(0, 2);
|
|
||||||
assert_eq!(vals(&ring), vec![2, 1, 0]);
|
|
||||||
assert!(ring[0].locked);
|
|
||||||
assert!(!ring[1].locked);
|
|
||||||
assert!(ring[2].locked);
|
|
||||||
|
|
||||||
// Both unlocked
|
|
||||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false)]);
|
|
||||||
ring.swap_respecting_locks(0, 2);
|
|
||||||
assert_eq!(vals(&ring), vec![2, 1, 0]);
|
|
||||||
assert!(!ring[0].locked);
|
|
||||||
assert!(ring[1].locked);
|
|
||||||
assert!(!ring[2].locked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
pub struct LockedDeque<'a, T> {
|
||||||
|
deque: &'a mut VecDeque<T>,
|
||||||
|
locked_indices: &'a mut BTreeSet<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: PartialEq> LockedDeque<'a, T> {
|
||||||
|
pub fn new(deque: &'a mut VecDeque<T>, locked_indices: &'a mut BTreeSet<usize>) -> Self {
|
||||||
|
Self {
|
||||||
|
deque,
|
||||||
|
locked_indices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, index: usize, value: T) -> usize {
|
||||||
|
insert_respecting_locks(self.deque, self.locked_indices, index, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, index: usize) -> Option<T> {
|
||||||
|
remove_respecting_locks(self.deque, self.locked_indices, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_respecting_locks<T>(
|
||||||
|
deque: &mut VecDeque<T>,
|
||||||
|
locked_idx: &mut BTreeSet<usize>,
|
||||||
|
idx: usize,
|
||||||
|
value: T,
|
||||||
|
) -> usize {
|
||||||
|
if idx == deque.len() {
|
||||||
|
deque.push_back(value);
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_deque = VecDeque::with_capacity(deque.len() + 1);
|
||||||
|
let mut temp_locked_deque = VecDeque::new();
|
||||||
|
let mut j = 0;
|
||||||
|
let mut corrected_idx = idx;
|
||||||
|
|
||||||
|
for (i, el) in deque.drain(..).enumerate() {
|
||||||
|
if i == idx {
|
||||||
|
corrected_idx = j;
|
||||||
|
}
|
||||||
|
if locked_idx.contains(&i) {
|
||||||
|
temp_locked_deque.push_back(el);
|
||||||
|
} else {
|
||||||
|
new_deque.push_back(el);
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_deque.insert(corrected_idx, value);
|
||||||
|
|
||||||
|
for (locked_el, locked_idx) in temp_locked_deque.into_iter().zip(locked_idx.iter()) {
|
||||||
|
new_deque.insert(*locked_idx, locked_el);
|
||||||
|
if *locked_idx <= corrected_idx {
|
||||||
|
corrected_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*deque = new_deque;
|
||||||
|
|
||||||
|
corrected_idx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_respecting_locks<T>(
|
||||||
|
deque: &mut VecDeque<T>,
|
||||||
|
locked_idx: &mut BTreeSet<usize>,
|
||||||
|
idx: usize,
|
||||||
|
) -> Option<T> {
|
||||||
|
if idx >= deque.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let final_size = deque.len() - 1;
|
||||||
|
|
||||||
|
let mut new_deque = VecDeque::with_capacity(final_size);
|
||||||
|
let mut temp_locked_deque = VecDeque::new();
|
||||||
|
let mut removed = None;
|
||||||
|
let mut removed_locked_idx = None;
|
||||||
|
|
||||||
|
for (i, el) in deque.drain(..).enumerate() {
|
||||||
|
if i == idx {
|
||||||
|
removed = Some(el);
|
||||||
|
removed_locked_idx = locked_idx.contains(&i).then_some(i);
|
||||||
|
} else if locked_idx.contains(&i) {
|
||||||
|
temp_locked_deque.push_back(el);
|
||||||
|
} else {
|
||||||
|
new_deque.push_back(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(i) = removed_locked_idx {
|
||||||
|
let mut above = locked_idx.split_off(&i);
|
||||||
|
above.pop_first();
|
||||||
|
locked_idx.extend(above.into_iter().map(|i| i - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
while locked_idx.last().is_some_and(|i| *i >= final_size) {
|
||||||
|
locked_idx.pop_last();
|
||||||
|
}
|
||||||
|
|
||||||
|
let extra_invalid_idx = (new_deque.len()
|
||||||
|
..(new_deque.len() + temp_locked_deque.len() - locked_idx.len()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (locked_el, locked_idx) in temp_locked_deque
|
||||||
|
.into_iter()
|
||||||
|
.zip(locked_idx.iter().chain(extra_invalid_idx.iter()))
|
||||||
|
{
|
||||||
|
new_deque.insert(*locked_idx, locked_el);
|
||||||
|
}
|
||||||
|
|
||||||
|
*deque = new_deque;
|
||||||
|
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_respecting_locks() {
|
||||||
|
// Test case 1: Basic insertion with locked index
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(2); // Lock index 2
|
||||||
|
|
||||||
|
// Insert at index 0, should shift elements while keeping index 2 locked
|
||||||
|
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![99, 0, 2, 1, 3, 4]));
|
||||||
|
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 2: Insert at a locked index
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(2); // Lock index 2
|
||||||
|
|
||||||
|
// Try to insert at locked index 2, should insert at index 3 instead
|
||||||
|
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
|
||||||
|
assert_eq!(actual_index, 3);
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 99, 3, 4]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 3: Multiple locked indices
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(1); // Lock index 1
|
||||||
|
locked.insert(3); // Lock index 3
|
||||||
|
|
||||||
|
// Insert at index 0, should maintain locked indices
|
||||||
|
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![99, 1, 0, 3, 2, 4]));
|
||||||
|
// Elements '1' and '3' remain at indices 1 and 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 4: Insert at end
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(2); // Lock index 2
|
||||||
|
|
||||||
|
// Insert at end of deque
|
||||||
|
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 5, 99);
|
||||||
|
assert_eq!(actual_index, 5);
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 5: Empty deque
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::new();
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
|
||||||
|
// Insert into empty deque
|
||||||
|
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||||
|
assert_eq!(actual_index, 0);
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![99]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 6: All indices locked
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
for i in 0..5 {
|
||||||
|
locked.insert(i); // Lock all indices
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to insert at index 2, should insert at the end
|
||||||
|
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
|
||||||
|
assert_eq!(actual_index, 5);
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 7: Consecutive locked indices
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(2); // Lock index 2
|
||||||
|
locked.insert(3); // Lock index 3
|
||||||
|
|
||||||
|
// Insert at index 1, should maintain consecutive locked indices
|
||||||
|
insert_respecting_locks(&mut deque, &mut locked, 1, 99);
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![0, 99, 2, 3, 1, 4]));
|
||||||
|
// Elements '2' and '3' remain at indices 2 and 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_remove_respecting_locks() {
|
||||||
|
// Test case 1: Remove a non-locked index before a locked index
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(2); // Lock index 2
|
||||||
|
|
||||||
|
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||||
|
assert_eq!(removed, Some(0));
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![1, 3, 2, 4]));
|
||||||
|
assert!(locked.contains(&2)); // Index 2 should still be locked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 2: Remove a locked index
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(2); // Lock index 2
|
||||||
|
|
||||||
|
let removed = remove_respecting_locks(&mut deque, &mut locked, 2);
|
||||||
|
assert_eq!(removed, Some(2));
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![0, 1, 3, 4]));
|
||||||
|
assert!(!locked.contains(&2)); // Index 2 should be unlocked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 3: Remove an index after a locked index
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(1); // Lock index 1
|
||||||
|
|
||||||
|
let removed = remove_respecting_locks(&mut deque, &mut locked, 3);
|
||||||
|
assert_eq!(removed, Some(3));
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 4]));
|
||||||
|
assert!(locked.contains(&1)); // Index 1 should still be locked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 4: Multiple locked indices
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(1); // Lock index 1
|
||||||
|
locked.insert(3); // Lock index 3
|
||||||
|
|
||||||
|
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||||
|
assert_eq!(removed, Some(0));
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![2, 1, 4, 3]));
|
||||||
|
assert!(locked.contains(&1) && locked.contains(&3)); // Both indices should still be locked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 5: Remove the last element
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(2); // Lock index 2
|
||||||
|
|
||||||
|
let removed = remove_respecting_locks(&mut deque, &mut locked, 4);
|
||||||
|
assert_eq!(removed, Some(4));
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3]));
|
||||||
|
assert!(locked.contains(&2)); // Index 2 should still be locked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 6: Invalid index
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(2); // Lock index 2
|
||||||
|
|
||||||
|
let removed = remove_respecting_locks(&mut deque, &mut locked, 10);
|
||||||
|
assert_eq!(removed, None);
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4])); // Deque unchanged
|
||||||
|
assert!(locked.contains(&2)); // Lock unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 7: Remove enough elements to make a locked index invalid
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(2); // Lock index 2
|
||||||
|
|
||||||
|
remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![1, 2]));
|
||||||
|
assert!(!locked.contains(&2)); // Index 2 should now be invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 8: Removing an element before multiple locked indices
|
||||||
|
{
|
||||||
|
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4, 5]);
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
locked.insert(2); // Lock index 2
|
||||||
|
locked.insert(4); // Lock index 4
|
||||||
|
|
||||||
|
let removed = remove_respecting_locks(&mut deque, &mut locked, 1);
|
||||||
|
assert_eq!(removed, Some(1));
|
||||||
|
assert_eq!(deque, VecDeque::from(vec![0, 3, 2, 5, 4]));
|
||||||
|
assert!(locked.contains(&2) && locked.contains(&4)); // Both indices should still be locked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-60
@@ -500,7 +500,7 @@ impl Monitor {
|
|||||||
if workspaces.get(idx).is_none() {
|
if workspaces.get(idx).is_none() {
|
||||||
workspaces.resize(idx + 1, Workspace::default());
|
workspaces.resize(idx + 1, Workspace::default());
|
||||||
}
|
}
|
||||||
self.set_last_focused_workspace(Some(self.workspaces.focused_idx()));
|
|
||||||
self.workspaces.focus(idx);
|
self.workspaces.focus(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,25 +631,6 @@ mod tests {
|
|||||||
assert_eq!(m.workspaces().len(), 0);
|
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]
|
#[test]
|
||||||
fn test_focus_workspace() {
|
fn test_focus_workspace() {
|
||||||
let mut m = Monitor::new(
|
let mut m = Monitor::new(
|
||||||
@@ -757,46 +738,6 @@ mod tests {
|
|||||||
assert_eq!(m.focused_workspace().unwrap().containers().len(), 2);
|
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]
|
#[test]
|
||||||
fn test_ensure_workspace_count_workspace_contains_two_workspaces() {
|
fn test_ensure_workspace_count_workspace_contains_two_workspaces() {
|
||||||
let mut m = Monitor::new(
|
let mut m = Monitor::new(
|
||||||
|
|||||||
@@ -801,7 +801,7 @@ mod tests {
|
|||||||
let wm = match WindowManager::new(receiver, Some(socket_path.clone())) {
|
let wm = match WindowManager::new(receiver, Some(socket_path.clone())) {
|
||||||
Ok(manager) => manager,
|
Ok(manager) => manager,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
panic!("Failed to create WindowManager: {e}");
|
panic!("Failed to create WindowManager: {}", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -829,7 +829,7 @@ mod tests {
|
|||||||
Ok(notification) => {
|
Ok(notification) => {
|
||||||
assert_eq!(notification, MonitorNotification::ResolutionScalingChanged);
|
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 {
|
for _ in 0..20 {
|
||||||
let notification = match receiver.try_recv() {
|
let notification = match receiver.try_recv() {
|
||||||
Ok(notification) => notification,
|
Ok(notification) => notification,
|
||||||
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
|
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
notification,
|
notification,
|
||||||
@@ -960,7 +960,7 @@ mod tests {
|
|||||||
Ok(notification) => {
|
Ok(notification) => {
|
||||||
assert_eq!(notification, MonitorNotification::DisplayConnectionChange);
|
assert_eq!(notification, MonitorNotification::DisplayConnectionChange);
|
||||||
}
|
}
|
||||||
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
|
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ use crate::core::StateQuery;
|
|||||||
use crate::core::WindowContainerBehaviour;
|
use crate::core::WindowContainerBehaviour;
|
||||||
use crate::core::WindowKind;
|
use crate::core::WindowKind;
|
||||||
use crate::current_virtual_desktop;
|
use crate::current_virtual_desktop;
|
||||||
use crate::default_layout::LayoutOptions;
|
|
||||||
use crate::default_layout::ScrollingLayoutOptions;
|
|
||||||
use crate::monitor::MonitorInformation;
|
use crate::monitor::MonitorInformation;
|
||||||
use crate::notify_subscribers;
|
use crate::notify_subscribers;
|
||||||
use crate::stackbar_manager;
|
use crate::stackbar_manager;
|
||||||
@@ -209,6 +207,25 @@ impl WindowManager {
|
|||||||
// We don't have From implemented for &mut WindowManager
|
// We don't have From implemented for &mut WindowManager
|
||||||
let initial_state = State::from(self.as_ref());
|
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;
|
let mut force_update_borders = false;
|
||||||
match message {
|
match message {
|
||||||
SocketMessage::Promote => self.promote_container_to_front()?,
|
SocketMessage::Promote => self.promote_container_to_front()?,
|
||||||
@@ -330,7 +347,7 @@ impl WindowManager {
|
|||||||
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
|
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
|
||||||
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
|
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
|
||||||
SocketMessage::StackAll => self.stack_all()?,
|
SocketMessage::StackAll => self.stack_all()?,
|
||||||
SocketMessage::UnstackAll => self.unstack_all(true)?,
|
SocketMessage::UnstackAll => self.unstack_all()?,
|
||||||
SocketMessage::CycleStack(direction) => {
|
SocketMessage::CycleStack(direction) => {
|
||||||
self.cycle_container_window_in_direction(direction)?;
|
self.cycle_container_window_in_direction(direction)?;
|
||||||
}
|
}
|
||||||
@@ -374,9 +391,7 @@ impl WindowManager {
|
|||||||
.get_mut(workspace_idx)
|
.get_mut(workspace_idx)
|
||||||
.ok_or_eyre("no workspace at the given index")?;
|
.ok_or_eyre("no workspace at the given index")?;
|
||||||
|
|
||||||
if let Some(container) = workspace.containers_mut().get_mut(container_idx) {
|
workspace.locked_containers.insert(container_idx);
|
||||||
container.set_locked(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
SocketMessage::UnlockMonitorWorkspaceContainer(
|
SocketMessage::UnlockMonitorWorkspaceContainer(
|
||||||
monitor_idx,
|
monitor_idx,
|
||||||
@@ -393,9 +408,7 @@ impl WindowManager {
|
|||||||
.get_mut(workspace_idx)
|
.get_mut(workspace_idx)
|
||||||
.ok_or_eyre("no workspace at the given index")?;
|
.ok_or_eyre("no workspace at the given index")?;
|
||||||
|
|
||||||
if let Some(container) = workspace.containers_mut().get_mut(container_idx) {
|
workspace.locked_containers.remove(&container_idx);
|
||||||
container.set_locked(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
SocketMessage::ToggleLock => self.toggle_lock()?,
|
SocketMessage::ToggleLock => self.toggle_lock()?,
|
||||||
SocketMessage::ToggleFloat => self.toggle_float(false)?,
|
SocketMessage::ToggleFloat => self.toggle_float(false)?,
|
||||||
@@ -920,27 +933,6 @@ impl WindowManager {
|
|||||||
self.retile_all(true)?
|
self.retile_all(true)?
|
||||||
}
|
}
|
||||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
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::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||||
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
||||||
SocketMessage::ChangeLayoutCustom(ref path) => {
|
SocketMessage::ChangeLayoutCustom(ref path) => {
|
||||||
@@ -1289,7 +1281,6 @@ impl WindowManager {
|
|||||||
if i == focused_idx {
|
if i == focused_idx {
|
||||||
to_focus = Some(*window);
|
to_focus = Some(*window);
|
||||||
} else {
|
} else {
|
||||||
window.restore();
|
|
||||||
window.raise()?;
|
window.raise()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1297,7 +1288,6 @@ impl WindowManager {
|
|||||||
if let Some(focused_window) = &to_focus {
|
if let Some(focused_window) = &to_focus {
|
||||||
// The focused window should be the last one raised to make sure it is
|
// The focused window should be the last one raised to make sure it is
|
||||||
// on top
|
// on top
|
||||||
focused_window.restore();
|
|
||||||
focused_window.raise()?;
|
focused_window.raise()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1307,8 +1297,8 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(monocle) = workspace.monocle_container() {
|
if let Some(container) = workspace.monocle_container() {
|
||||||
if let Some(window) = monocle.focused_window() {
|
if let Some(window) = container.focused_window() {
|
||||||
window.lower()?;
|
window.lower()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1316,18 +1306,8 @@ impl WindowManager {
|
|||||||
WorkspaceLayer::Floating => {
|
WorkspaceLayer::Floating => {
|
||||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||||
|
|
||||||
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();
|
let focused_container_idx = workspace.focused_container_idx();
|
||||||
for (i, container) in workspace.containers_mut().iter_mut().enumerate()
|
for (i, container) in workspace.containers_mut().iter_mut().enumerate() {
|
||||||
{
|
|
||||||
if let Some(window) = container.focused_window() {
|
if let Some(window) = container.focused_window() {
|
||||||
if i == focused_container_idx {
|
if i == focused_container_idx {
|
||||||
to_focus = Some(*window);
|
to_focus = Some(*window);
|
||||||
@@ -1352,7 +1332,6 @@ impl WindowManager {
|
|||||||
window.lower()?;
|
window.lower()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if let Some(window) = to_focus {
|
if let Some(window) = to_focus {
|
||||||
window.focus(mouse_follows_focus)?;
|
window.focus(mouse_follows_focus)?;
|
||||||
@@ -1483,18 +1462,6 @@ 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())?;
|
reply.write_all(response.as_bytes())?;
|
||||||
@@ -1886,14 +1853,6 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
|
|||||||
self.retile_all(false)?;
|
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 => {
|
SocketMessage::ToggleWindowBasedWorkAreaOffset => {
|
||||||
let workspace = self.focused_workspace_mut()?;
|
let workspace = self.focused_workspace_mut()?;
|
||||||
workspace.set_apply_window_based_work_area_offset(
|
workspace.set_apply_window_based_work_area_offset(
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ use crate::window_manager_event::WindowManagerEvent;
|
|||||||
use crate::windows_api::WindowsApi;
|
use crate::windows_api::WindowsApi;
|
||||||
use crate::winevent::WinEvent;
|
use crate::winevent::WinEvent;
|
||||||
use crate::workspace::WorkspaceLayer;
|
use crate::workspace::WorkspaceLayer;
|
||||||
use crate::DefaultLayout;
|
|
||||||
use crate::Layout;
|
|
||||||
use crate::Notification;
|
use crate::Notification;
|
||||||
use crate::NotificationEvent;
|
use crate::NotificationEvent;
|
||||||
use crate::State;
|
use crate::State;
|
||||||
@@ -303,11 +301,7 @@ impl WindowManager {
|
|||||||
// don't want to trigger the full workspace updates when there are no managed
|
// 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
|
// containers - this makes floating windows on empty workspaces go into very
|
||||||
// annoying focus change loops which prevents users from interacting with them
|
// annoying focus change loops which prevents users from interacting with them
|
||||||
if !matches!(
|
if !self.focused_workspace()?.containers().is_empty() {
|
||||||
self.focused_workspace()?.layout(),
|
|
||||||
Layout::Default(DefaultLayout::Scrolling)
|
|
||||||
) && !self.focused_workspace()?.containers().is_empty()
|
|
||||||
{
|
|
||||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,14 +328,6 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
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) => {
|
Some(idx) => {
|
||||||
if let Some(_window) = workspace.floating_windows().get(idx) {
|
if let Some(_window) = workspace.floating_windows().get(idx) {
|
||||||
@@ -353,11 +339,10 @@ impl WindowManager {
|
|||||||
WindowManagerEvent::Show(_, window)
|
WindowManagerEvent::Show(_, window)
|
||||||
| WindowManagerEvent::Manage(window)
|
| WindowManagerEvent::Manage(window)
|
||||||
| WindowManagerEvent::Uncloak(_, window) => {
|
| WindowManagerEvent::Uncloak(_, window) => {
|
||||||
if matches!(event, WindowManagerEvent::Uncloak(_, _))
|
if matches!(event, WindowManagerEvent::Uncloak(_, _)) && self.uncloak_to_ignore >= 1
|
||||||
&& self.uncloack_to_ignore >= 1
|
|
||||||
{
|
{
|
||||||
tracing::info!("ignoring uncloak after monocle move by mouse across monitors");
|
tracing::info!("ignoring uncloak after monocle move by mouse across monitors");
|
||||||
self.uncloack_to_ignore = self.uncloack_to_ignore.saturating_sub(1);
|
self.uncloak_to_ignore = self.uncloak_to_ignore.saturating_sub(1);
|
||||||
} else {
|
} else {
|
||||||
let focused_monitor_idx = self.focused_monitor_idx();
|
let focused_monitor_idx = self.focused_monitor_idx();
|
||||||
let focused_workspace_idx =
|
let focused_workspace_idx =
|
||||||
@@ -416,6 +401,7 @@ impl WindowManager {
|
|||||||
let workspace = self.focused_workspace_mut()?;
|
let workspace = self.focused_workspace_mut()?;
|
||||||
let workspace_contains_window = workspace.contains_window(window.hwnd);
|
let workspace_contains_window = workspace.contains_window(window.hwnd);
|
||||||
let monocle_container = workspace.monocle_container().clone();
|
let monocle_container = workspace.monocle_container().clone();
|
||||||
|
let mut workspace_layer = *workspace.layer();
|
||||||
|
|
||||||
if !workspace_contains_window && needs_reconciliation.is_none() {
|
if !workspace_contains_window && needs_reconciliation.is_none() {
|
||||||
let floating_applications = FLOATING_APPLICATIONS.lock();
|
let floating_applications = FLOATING_APPLICATIONS.lock();
|
||||||
@@ -459,6 +445,7 @@ impl WindowManager {
|
|||||||
placement.should_center() && workspace.tile;
|
placement.should_center() && workspace.tile;
|
||||||
workspace.floating_windows_mut().push_back(window);
|
workspace.floating_windows_mut().push_back(window);
|
||||||
workspace.set_layer(WorkspaceLayer::Floating);
|
workspace.set_layer(WorkspaceLayer::Floating);
|
||||||
|
workspace_layer = *workspace.layer();
|
||||||
if center_spawned_floats {
|
if center_spawned_floats {
|
||||||
let mut floating_window = window;
|
let mut floating_window = window;
|
||||||
floating_window.center(
|
floating_window.center(
|
||||||
@@ -472,6 +459,7 @@ impl WindowManager {
|
|||||||
WindowContainerBehaviour::Create => {
|
WindowContainerBehaviour::Create => {
|
||||||
workspace.new_container_for_window(window);
|
workspace.new_container_for_window(window);
|
||||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||||
|
workspace_layer = *workspace.layer();
|
||||||
self.update_focused_workspace(false, false)?;
|
self.update_focused_workspace(false, false)?;
|
||||||
}
|
}
|
||||||
WindowContainerBehaviour::Append => {
|
WindowContainerBehaviour::Append => {
|
||||||
@@ -482,6 +470,7 @@ impl WindowManager {
|
|||||||
})?
|
})?
|
||||||
.add_window(window);
|
.add_window(window);
|
||||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||||
|
workspace_layer = *workspace.layer();
|
||||||
self.update_focused_workspace(true, false)?;
|
self.update_focused_workspace(true, false)?;
|
||||||
stackbar_manager::send_notification();
|
stackbar_manager::send_notification();
|
||||||
}
|
}
|
||||||
@@ -514,10 +503,9 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let workspace = self.focused_workspace()?;
|
if !monocle_window_event
|
||||||
if !(monocle_window_event
|
|
||||||
|| workspace.layer() != &WorkspaceLayer::Tiling)
|
|
||||||
&& monocle_container.is_some()
|
&& monocle_container.is_some()
|
||||||
|
&& matches!(workspace_layer, WorkspaceLayer::Tiling)
|
||||||
{
|
{
|
||||||
window.hide();
|
window.hide();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ impl<T> Ring<T> {
|
|||||||
pub fn focused_mut(&mut self) -> Option<&mut T> {
|
pub fn focused_mut(&mut self) -> Option<&mut T> {
|
||||||
self.elements.get_mut(self.focused)
|
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 {
|
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_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
|
||||||
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
|
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
|
||||||
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
|
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
|
||||||
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Title);
|
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
|
||||||
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::Never);
|
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::OnStack);
|
||||||
|
|
||||||
pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
|||||||
@@ -58,19 +58,15 @@ use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
|
|||||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
|
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
|
|
||||||
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
|
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
|
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::WNDCLASSW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||||
@@ -316,16 +312,6 @@ impl Stackbar {
|
|||||||
) -> LRESULT {
|
) -> LRESULT {
|
||||||
unsafe {
|
unsafe {
|
||||||
match msg {
|
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 => {
|
WM_LBUTTONDOWN => {
|
||||||
let stackbars_containers = STACKBARS_CONTAINERS.lock();
|
let stackbars_containers = STACKBARS_CONTAINERS.lock();
|
||||||
if let Some(container) = stackbars_containers.get(&(hwnd.0 as isize)) {
|
if let Some(container) = stackbars_containers.get(&(hwnd.0 as isize)) {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ use crate::core::StackbarMode;
|
|||||||
use crate::core::WindowContainerBehaviour;
|
use crate::core::WindowContainerBehaviour;
|
||||||
use crate::core::WindowManagementBehaviour;
|
use crate::core::WindowManagementBehaviour;
|
||||||
use crate::current_virtual_desktop;
|
use crate::current_virtual_desktop;
|
||||||
use crate::default_layout::LayoutOptions;
|
|
||||||
use crate::monitor;
|
use crate::monitor;
|
||||||
use crate::monitor::Monitor;
|
use crate::monitor::Monitor;
|
||||||
use crate::monitor_reconciliator;
|
use crate::monitor_reconciliator;
|
||||||
@@ -192,9 +191,6 @@ pub struct WorkspaceConfig {
|
|||||||
/// Layout (default: BSP)
|
/// Layout (default: BSP)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub layout: Option<DefaultLayout>,
|
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)
|
/// END OF LIFE FEATURE: Custom Layout (default: None)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde_as(as = "Option<ResolvedPathBuf>")]
|
#[serde_as(as = "Option<ResolvedPathBuf>")]
|
||||||
@@ -218,9 +214,6 @@ pub struct WorkspaceConfig {
|
|||||||
/// Permanent workspace application rules
|
/// Permanent workspace application rules
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub workspace_rules: Option<Vec<MatchingRule>>,
|
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)
|
/// Apply this monitor's window-based work area offset (default: true)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub apply_window_based_work_area_offset: Option<bool>,
|
pub apply_window_based_work_area_offset: Option<bool>,
|
||||||
@@ -233,9 +226,6 @@ pub struct WorkspaceConfig {
|
|||||||
/// Enable or disable float override, which makes it so every new window opens in floating mode (default: false)
|
/// 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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub float_override: Option<bool>,
|
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)
|
/// Specify an axis on which to flip the selected layout (default: None)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub layout_flip: Option<Axis>,
|
pub layout_flip: Option<Axis>,
|
||||||
@@ -284,8 +274,6 @@ impl From<&Workspace> for WorkspaceConfig {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let tile = if *value.tile() { None } else { Some(false) };
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: value
|
name: value
|
||||||
.name()
|
.name()
|
||||||
@@ -298,7 +286,6 @@ impl From<&Workspace> for WorkspaceConfig {
|
|||||||
Layout::Custom(_) => None,
|
Layout::Custom(_) => None,
|
||||||
})
|
})
|
||||||
.flatten(),
|
.flatten(),
|
||||||
layout_options: value.layout_options(),
|
|
||||||
custom_layout: value
|
custom_layout: value
|
||||||
.workspace_config()
|
.workspace_config()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -318,12 +305,10 @@ impl From<&Workspace> for WorkspaceConfig {
|
|||||||
.workspace_config()
|
.workspace_config()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|c| c.workspace_rules.clone()),
|
.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()),
|
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
|
||||||
window_container_behaviour: *value.window_container_behaviour(),
|
window_container_behaviour: *value.window_container_behaviour(),
|
||||||
window_container_behaviour_rules: Option::from(window_container_behaviour_rules),
|
window_container_behaviour_rules: Option::from(window_container_behaviour_rules),
|
||||||
float_override: *value.float_override(),
|
float_override: *value.float_override(),
|
||||||
tile,
|
|
||||||
layout_flip: value.layout_flip(),
|
layout_flip: value.layout_flip(),
|
||||||
floating_layer_behaviour: value.floating_layer_behaviour(),
|
floating_layer_behaviour: value.floating_layer_behaviour(),
|
||||||
wallpaper: None,
|
wallpaper: None,
|
||||||
@@ -412,7 +397,7 @@ pub enum AppSpecificConfigurationPath {
|
|||||||
#[serde_with::serde_as]
|
#[serde_with::serde_as]
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
/// The `komorebi.json` static configuration file reference for `v0.1.38`
|
/// The `komorebi.json` static configuration file reference for `v0.1.37`
|
||||||
pub struct StaticConfig {
|
pub struct StaticConfig {
|
||||||
/// DEPRECATED from v0.1.22: no longer required
|
/// DEPRECATED from v0.1.22: no longer required
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -822,7 +807,7 @@ pub struct StackbarConfig {
|
|||||||
/// Stackbar label
|
/// Stackbar label
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub label: Option<StackbarLabel>,
|
pub label: Option<StackbarLabel>,
|
||||||
/// Stackbar mode (default: Never)
|
/// Stackbar mode
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub mode: Option<StackbarMode>,
|
pub mode: Option<StackbarMode>,
|
||||||
/// Stackbar tab configuration options
|
/// Stackbar tab configuration options
|
||||||
@@ -1314,7 +1299,7 @@ impl StaticConfig {
|
|||||||
has_pending_raise_op: false,
|
has_pending_raise_op: false,
|
||||||
pending_move_op: Arc::new(None),
|
pending_move_op: Arc::new(None),
|
||||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||||
uncloack_to_ignore: 0,
|
uncloak_to_ignore: 0,
|
||||||
known_hwnds: HashMap::new(),
|
known_hwnds: HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1346,7 +1331,7 @@ impl StaticConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn postload(path: &PathBuf, wm: &Arc<Mutex<WindowManager>>) -> Result<()> {
|
pub fn postload(path: &PathBuf, wm: &Arc<Mutex<WindowManager>>) -> Result<()> {
|
||||||
let mut value = Self::read(path)?;
|
let value = Self::read(path)?;
|
||||||
let mut wm = wm.lock();
|
let mut wm = wm.lock();
|
||||||
|
|
||||||
let configs_with_preference: Vec<_> =
|
let configs_with_preference: Vec<_> =
|
||||||
@@ -1357,8 +1342,6 @@ impl StaticConfig {
|
|||||||
workspace_matching_rules.clear();
|
workspace_matching_rules.clear();
|
||||||
drop(workspace_matching_rules);
|
drop(workspace_matching_rules);
|
||||||
|
|
||||||
let monitor_count = wm.monitors().len();
|
|
||||||
|
|
||||||
let offset = wm.work_area_offset;
|
let offset = wm.work_area_offset;
|
||||||
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
|
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
|
||||||
let preferred_config_idx = {
|
let preferred_config_idx = {
|
||||||
@@ -1388,8 +1371,8 @@ impl StaticConfig {
|
|||||||
});
|
});
|
||||||
if let Some(monitor_config) = value
|
if let Some(monitor_config) = value
|
||||||
.monitors
|
.monitors
|
||||||
.as_mut()
|
.as_ref()
|
||||||
.and_then(|ms| idx.and_then(|i| ms.get_mut(i)))
|
.and_then(|ms| idx.and_then(|i| ms.get(i)))
|
||||||
{
|
{
|
||||||
if let Some(used_config_idx) = idx {
|
if let Some(used_config_idx) = idx {
|
||||||
configs_used.push(used_config_idx);
|
configs_used.push(used_config_idx);
|
||||||
@@ -1412,14 +1395,7 @@ impl StaticConfig {
|
|||||||
|
|
||||||
monitor.update_workspaces_globals(offset);
|
monitor.update_workspaces_globals(offset);
|
||||||
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
|
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||||
if let Some(workspace_config) = monitor_config.workspaces.get_mut(j) {
|
if let Some(workspace_config) = monitor_config.workspaces.get(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)?;
|
ws.load_static_config(workspace_config)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1936,13 +1912,11 @@ mod tests {
|
|||||||
use crate::WorkspaceConfig;
|
use crate::WorkspaceConfig;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "this fails on github actions due to rate limiting changes introduced in may 2025"]
|
|
||||||
fn backwards_compat() {
|
fn backwards_compat() {
|
||||||
let root = vec!["0.1.17", "0.1.18", "0.1.19"];
|
let root = vec!["0.1.17", "0.1.18", "0.1.19"];
|
||||||
let docs = vec![
|
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.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.35",
|
"0.1.28", "0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33", "0.1.34",
|
||||||
"0.1.36", "0.1.37",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut versions = vec![];
|
let mut versions = vec![];
|
||||||
|
|||||||
+19
-513
@@ -120,13 +120,13 @@ pub struct WindowManager {
|
|||||||
pub has_pending_raise_op: bool,
|
pub has_pending_raise_op: bool,
|
||||||
pub pending_move_op: Arc<Option<(usize, usize, isize)>>,
|
pub pending_move_op: Arc<Option<(usize, usize, isize)>>,
|
||||||
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
|
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
|
||||||
pub uncloack_to_ignore: usize,
|
pub uncloak_to_ignore: usize,
|
||||||
/// Maps each known window hwnd to the (monitor, workspace) index pair managing it
|
/// Maps each known window hwnd to the (monitor, workspace) index pair managing it
|
||||||
pub known_hwnds: HashMap<isize, (usize, usize)>,
|
pub known_hwnds: HashMap<isize, (usize, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub monitors: Ring<Monitor>,
|
pub monitors: Ring<Monitor>,
|
||||||
@@ -329,7 +329,6 @@ impl From<&WindowManager> for State {
|
|||||||
maximized_window_restore_idx: workspace.maximized_window_restore_idx,
|
maximized_window_restore_idx: workspace.maximized_window_restore_idx,
|
||||||
floating_windows: workspace.floating_windows.clone(),
|
floating_windows: workspace.floating_windows.clone(),
|
||||||
layout: workspace.layout.clone(),
|
layout: workspace.layout.clone(),
|
||||||
layout_options: workspace.layout_options,
|
|
||||||
layout_rules: workspace.layout_rules.clone(),
|
layout_rules: workspace.layout_rules.clone(),
|
||||||
layout_flip: workspace.layout_flip,
|
layout_flip: workspace.layout_flip,
|
||||||
workspace_padding: workspace.workspace_padding,
|
workspace_padding: workspace.workspace_padding,
|
||||||
@@ -337,7 +336,6 @@ impl From<&WindowManager> for State {
|
|||||||
latest_layout: workspace.latest_layout.clone(),
|
latest_layout: workspace.latest_layout.clone(),
|
||||||
resize_dimensions: workspace.resize_dimensions.clone(),
|
resize_dimensions: workspace.resize_dimensions.clone(),
|
||||||
tile: workspace.tile,
|
tile: workspace.tile,
|
||||||
work_area_offset: workspace.work_area_offset,
|
|
||||||
apply_window_based_work_area_offset: workspace
|
apply_window_based_work_area_offset: workspace
|
||||||
.apply_window_based_work_area_offset,
|
.apply_window_based_work_area_offset,
|
||||||
window_container_behaviour: workspace.window_container_behaviour,
|
window_container_behaviour: workspace.window_container_behaviour,
|
||||||
@@ -348,6 +346,7 @@ impl From<&WindowManager> for State {
|
|||||||
layer: workspace.layer,
|
layer: workspace.layer,
|
||||||
floating_layer_behaviour: workspace.floating_layer_behaviour,
|
floating_layer_behaviour: workspace.floating_layer_behaviour,
|
||||||
globals: workspace.globals,
|
globals: workspace.globals,
|
||||||
|
locked_containers: workspace.locked_containers.clone(),
|
||||||
wallpaper: workspace.wallpaper.clone(),
|
wallpaper: workspace.wallpaper.clone(),
|
||||||
workspace_config: None,
|
workspace_config: None,
|
||||||
})
|
})
|
||||||
@@ -448,7 +447,7 @@ impl WindowManager {
|
|||||||
has_pending_raise_op: false,
|
has_pending_raise_op: false,
|
||||||
pending_move_op: Arc::new(None),
|
pending_move_op: Arc::new(None),
|
||||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||||
uncloack_to_ignore: 0,
|
uncloak_to_ignore: 0,
|
||||||
known_hwnds: HashMap::new(),
|
known_hwnds: HashMap::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -822,7 +821,7 @@ impl WindowManager {
|
|||||||
target_workspace_idx: usize,
|
target_workspace_idx: usize,
|
||||||
floating: bool,
|
floating: bool,
|
||||||
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
|
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
|
||||||
) {
|
) -> () {
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"{} should be on monitor {}, workspace {}",
|
"{} should be on monitor {}, workspace {}",
|
||||||
window_title,
|
window_title,
|
||||||
@@ -1235,7 +1234,7 @@ impl WindowManager {
|
|||||||
// That workspace reconciliation would focus the window on the origin monitor.
|
// That workspace reconciliation would focus the window on the origin monitor.
|
||||||
// So we need to ignore the uncloak events produced by the origin workspace
|
// So we need to ignore the uncloak events produced by the origin workspace
|
||||||
// restore to avoid that issue.
|
// restore to avoid that issue.
|
||||||
self.uncloack_to_ignore = uncloack_amount;
|
self.uncloak_to_ignore = uncloack_amount;
|
||||||
}
|
}
|
||||||
} else if origin_workspace
|
} else if origin_workspace
|
||||||
.maximized_window()
|
.maximized_window()
|
||||||
@@ -1315,29 +1314,6 @@ impl WindowManager {
|
|||||||
let (origin_monitor_idx, origin_workspace_idx, origin_container_idx) = origin;
|
let (origin_monitor_idx, origin_workspace_idx, origin_container_idx) = origin;
|
||||||
let (target_monitor_idx, target_workspace_idx, target_container_idx) = target;
|
let (target_monitor_idx, target_workspace_idx, target_container_idx) = target;
|
||||||
|
|
||||||
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"))?
|
|
||||||
.containers()
|
|
||||||
.get(origin_container_idx)
|
|
||||||
.is_some();
|
|
||||||
|
|
||||||
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"))?
|
|
||||||
.containers()
|
|
||||||
.get(origin_container_idx)
|
|
||||||
.is_some();
|
|
||||||
|
|
||||||
if origin_container_is_valid && target_container_is_valid {
|
|
||||||
let origin_container = self
|
let origin_container = self
|
||||||
.monitors_mut()
|
.monitors_mut()
|
||||||
.get_mut(origin_monitor_idx)
|
.get_mut(origin_monitor_idx)
|
||||||
@@ -1376,7 +1352,6 @@ impl WindowManager {
|
|||||||
.containers_mut()
|
.containers_mut()
|
||||||
.insert(origin_container_idx, target_container);
|
.insert(origin_container_idx, target_container);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1580,9 +1555,6 @@ impl WindowManager {
|
|||||||
workspace.container_padding(),
|
workspace.container_padding(),
|
||||||
workspace.layout_flip(),
|
workspace.layout_flip(),
|
||||||
&[],
|
&[],
|
||||||
workspace.focused_container_idx(),
|
|
||||||
workspace.layout_options(),
|
|
||||||
workspace.latest_layout(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut direction = direction;
|
let mut direction = direction;
|
||||||
@@ -2960,8 +2932,6 @@ impl WindowManager {
|
|||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn stack_all(&mut self) -> Result<()> {
|
pub fn stack_all(&mut self) -> Result<()> {
|
||||||
self.unstack_all(false)?;
|
|
||||||
|
|
||||||
self.handle_unmanaged_window_behaviour()?;
|
self.handle_unmanaged_window_behaviour()?;
|
||||||
tracing::info!("stacking all windows on workspace");
|
tracing::info!("stacking all windows on workspace");
|
||||||
|
|
||||||
@@ -2988,7 +2958,7 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn unstack_all(&mut self, update_workspace: bool) -> Result<()> {
|
pub fn unstack_all(&mut self) -> Result<()> {
|
||||||
self.handle_unmanaged_window_behaviour()?;
|
self.handle_unmanaged_window_behaviour()?;
|
||||||
tracing::info!("unstacking all windows in container");
|
tracing::info!("unstacking all windows in container");
|
||||||
|
|
||||||
@@ -3018,11 +2988,7 @@ impl WindowManager {
|
|||||||
workspace.focus_container_by_window(hwnd)?;
|
workspace.focus_container_by_window(hwnd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if update_workspace {
|
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
@@ -3165,10 +3131,6 @@ impl WindowManager {
|
|||||||
pub fn toggle_float(&mut self, force_float: bool) -> Result<()> {
|
pub fn toggle_float(&mut self, force_float: bool) -> Result<()> {
|
||||||
let hwnd = WindowsApi::foreground_window()?;
|
let hwnd = WindowsApi::foreground_window()?;
|
||||||
let workspace = self.focused_workspace_mut()?;
|
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;
|
let mut is_floating_window = false;
|
||||||
|
|
||||||
@@ -3192,10 +3154,14 @@ impl WindowManager {
|
|||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn toggle_lock(&mut self) -> Result<()> {
|
pub fn toggle_lock(&mut self) -> Result<()> {
|
||||||
let workspace = self.focused_workspace_mut()?;
|
let workspace = self.focused_workspace_mut()?;
|
||||||
if let Some(container) = workspace.focused_container_mut() {
|
let index = workspace.focused_container_idx();
|
||||||
// Toggle the locked flag
|
|
||||||
container.set_locked(!container.locked());
|
if workspace.locked_containers().contains(&index) {
|
||||||
|
workspace.locked_containers_mut().remove(&index);
|
||||||
|
} else {
|
||||||
|
workspace.locked_containers_mut().insert(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3358,16 +3324,8 @@ impl WindowManager {
|
|||||||
pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> Result<()> {
|
pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> Result<()> {
|
||||||
tracing::info!("changing layout");
|
tracing::info!("changing layout");
|
||||||
|
|
||||||
let monitor_count = self.monitors().len();
|
|
||||||
let workspace = self.focused_workspace_mut()?;
|
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() {
|
match workspace.layout() {
|
||||||
Layout::Default(_) => {}
|
Layout::Default(_) => {}
|
||||||
Layout::Custom(layout) => {
|
Layout::Custom(layout) => {
|
||||||
@@ -4330,44 +4288,6 @@ mod tests {
|
|||||||
assert_eq!(current_monitor_idx, 0);
|
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]
|
#[test]
|
||||||
fn test_focused_monitor_size() {
|
fn test_focused_monitor_size() {
|
||||||
let (mut wm, _test_context) = setup_window_manager();
|
let (mut wm, _test_context) = setup_window_manager();
|
||||||
@@ -4551,64 +4471,6 @@ 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]
|
#[test]
|
||||||
fn test_transfer_container() {
|
fn test_transfer_container() {
|
||||||
let (mut wm, _context) = setup_window_manager();
|
let (mut wm, _context) = setup_window_manager();
|
||||||
@@ -4773,44 +4635,6 @@ 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]
|
#[test]
|
||||||
fn cycle_container_window_in_direction() {
|
fn cycle_container_window_in_direction() {
|
||||||
let (mut wm, _context) = setup_window_manager();
|
let (mut wm, _context) = setup_window_manager();
|
||||||
@@ -4880,44 +4704,6 @@ 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]
|
#[test]
|
||||||
fn test_cycle_container_window_index_in_direction() {
|
fn test_cycle_container_window_index_in_direction() {
|
||||||
let (mut wm, _context) = setup_window_manager();
|
let (mut wm, _context) = setup_window_manager();
|
||||||
@@ -5076,71 +4862,6 @@ 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]
|
#[test]
|
||||||
fn test_swap_monitor_workspaces() {
|
fn test_swap_monitor_workspaces() {
|
||||||
let (mut wm, _context) = setup_window_manager();
|
let (mut wm, _context) = setup_window_manager();
|
||||||
@@ -5225,52 +4946,6 @@ 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]
|
#[test]
|
||||||
fn test_move_workspace_to_monitor() {
|
fn test_move_workspace_to_monitor() {
|
||||||
let (mut wm, _context) = setup_window_manager();
|
let (mut wm, _context) = setup_window_manager();
|
||||||
@@ -5293,7 +4968,7 @@ mod tests {
|
|||||||
// Should have 2 workspaces
|
// Should have 2 workspaces
|
||||||
assert_eq!(m.workspaces().len(), 2);
|
assert_eq!(m.workspaces().len(), 2);
|
||||||
|
|
||||||
// Add monitor to window manager
|
// Add monitor to workspace
|
||||||
wm.monitors_mut().push_back(m);
|
wm.monitors_mut().push_back(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5340,42 +5015,6 @@ 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]
|
#[test]
|
||||||
fn test_toggle_tiling() {
|
fn test_toggle_tiling() {
|
||||||
let (mut wm, _context) = setup_window_manager();
|
let (mut wm, _context) = setup_window_manager();
|
||||||
@@ -5447,8 +5086,7 @@ mod tests {
|
|||||||
{
|
{
|
||||||
// Ensure container 2 is not locked
|
// Ensure container 2 is not locked
|
||||||
let workspace = wm.focused_workspace_mut().unwrap();
|
let workspace = wm.focused_workspace_mut().unwrap();
|
||||||
assert_eq!(workspace.focused_container_idx(), 2);
|
assert!(!workspace.locked_containers().contains(&2));
|
||||||
assert!(!workspace.focused_container().unwrap().locked());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle lock on focused container
|
// Toggle lock on focused container
|
||||||
@@ -5457,7 +5095,7 @@ mod tests {
|
|||||||
{
|
{
|
||||||
// Ensure container 2 is locked
|
// Ensure container 2 is locked
|
||||||
let workspace = wm.focused_workspace_mut().unwrap();
|
let workspace = wm.focused_workspace_mut().unwrap();
|
||||||
assert!(workspace.focused_container().unwrap().locked());
|
assert!(workspace.locked_containers().contains(&2));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle lock on focused container
|
// Toggle lock on focused container
|
||||||
@@ -5466,7 +5104,7 @@ mod tests {
|
|||||||
{
|
{
|
||||||
// Ensure container 2 is not locked
|
// Ensure container 2 is not locked
|
||||||
let workspace = wm.focused_workspace_mut().unwrap();
|
let workspace = wm.focused_workspace_mut().unwrap();
|
||||||
assert!(!workspace.focused_container().unwrap().locked());
|
assert!(!workspace.locked_containers().contains(&2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5548,40 +5186,6 @@ 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]
|
#[test]
|
||||||
fn test_maximize_and_unmaximize_window() {
|
fn test_maximize_and_unmaximize_window() {
|
||||||
let (mut wm, _context) = setup_window_manager();
|
let (mut wm, _context) = setup_window_manager();
|
||||||
@@ -5728,41 +5332,6 @@ 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]
|
#[test]
|
||||||
fn test_monocle_on_and_monocle_off() {
|
fn test_monocle_on_and_monocle_off() {
|
||||||
let (mut wm, _context) = setup_window_manager();
|
let (mut wm, _context) = setup_window_manager();
|
||||||
@@ -5834,41 +5403,6 @@ 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]
|
#[test]
|
||||||
fn test_toggle_monocle() {
|
fn test_toggle_monocle() {
|
||||||
let (mut wm, _context) = setup_window_manager();
|
let (mut wm, _context) = setup_window_manager();
|
||||||
@@ -5940,34 +5474,6 @@ 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]
|
#[test]
|
||||||
fn test_ensure_named_workspace_for_monitor() {
|
fn test_ensure_named_workspace_for_monitor() {
|
||||||
let (mut wm, _context) = setup_window_manager();
|
let (mut wm, _context) = setup_window_manager();
|
||||||
|
|||||||
@@ -1180,7 +1180,6 @@ impl WindowsApi {
|
|||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn enable_focus_follows_mouse() -> Result<()> {
|
pub fn enable_focus_follows_mouse() -> Result<()> {
|
||||||
#[allow(clippy::manual_dangling_ptr)]
|
|
||||||
Self::system_parameters_info_w(
|
Self::system_parameters_info_w(
|
||||||
SPI_SETACTIVEWINDOWTRACKING,
|
SPI_SETACTIVEWINDOWTRACKING,
|
||||||
0,
|
0,
|
||||||
|
|||||||
+46
-108
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
@@ -15,8 +16,7 @@ use crate::core::DefaultLayout;
|
|||||||
use crate::core::Layout;
|
use crate::core::Layout;
|
||||||
use crate::core::OperationDirection;
|
use crate::core::OperationDirection;
|
||||||
use crate::core::Rect;
|
use crate::core::Rect;
|
||||||
use crate::default_layout::LayoutOptions;
|
use crate::locked_deque::LockedDeque;
|
||||||
use crate::lockable_sequence::LockableSequence;
|
|
||||||
use crate::ring::Ring;
|
use crate::ring::Ring;
|
||||||
use crate::should_act;
|
use crate::should_act;
|
||||||
use crate::stackbar_manager;
|
use crate::stackbar_manager;
|
||||||
@@ -70,8 +70,6 @@ pub struct Workspace {
|
|||||||
pub floating_windows: Ring<Window>,
|
pub floating_windows: Ring<Window>,
|
||||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||||
pub layout: Layout,
|
pub layout: Layout,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
|
||||||
pub layout_options: Option<LayoutOptions>,
|
|
||||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||||
pub layout_rules: Vec<(usize, Layout)>,
|
pub layout_rules: Vec<(usize, Layout)>,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
#[getset(get_copy = "pub", set = "pub")]
|
||||||
@@ -87,8 +85,6 @@ pub struct Workspace {
|
|||||||
#[getset(get = "pub", set = "pub")]
|
#[getset(get = "pub", set = "pub")]
|
||||||
pub tile: bool,
|
pub tile: bool,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
#[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,
|
pub apply_window_based_work_area_offset: bool,
|
||||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||||
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||||
@@ -104,6 +100,8 @@ pub struct Workspace {
|
|||||||
#[getset(get_copy = "pub", get_mut = "pub", set = "pub")]
|
#[getset(get_copy = "pub", get_mut = "pub", set = "pub")]
|
||||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
#[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>,
|
pub wallpaper: Option<Wallpaper>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[getset(get = "pub", set = "pub")]
|
#[getset(get = "pub", set = "pub")]
|
||||||
@@ -141,7 +139,6 @@ impl Default for Workspace {
|
|||||||
monocle_container_restore_idx: None,
|
monocle_container_restore_idx: None,
|
||||||
floating_windows: Ring::default(),
|
floating_windows: Ring::default(),
|
||||||
layout: Layout::Default(DefaultLayout::BSP),
|
layout: Layout::Default(DefaultLayout::BSP),
|
||||||
layout_options: None,
|
|
||||||
layout_rules: vec![],
|
layout_rules: vec![],
|
||||||
layout_flip: None,
|
layout_flip: None,
|
||||||
workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)),
|
workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)),
|
||||||
@@ -149,7 +146,6 @@ impl Default for Workspace {
|
|||||||
latest_layout: vec![],
|
latest_layout: vec![],
|
||||||
resize_dimensions: vec![],
|
resize_dimensions: vec![],
|
||||||
tile: true,
|
tile: true,
|
||||||
work_area_offset: None,
|
|
||||||
apply_window_based_work_area_offset: true,
|
apply_window_based_work_area_offset: true,
|
||||||
window_container_behaviour: None,
|
window_container_behaviour: None,
|
||||||
window_container_behaviour_rules: None,
|
window_container_behaviour_rules: None,
|
||||||
@@ -158,6 +154,7 @@ impl Default for Workspace {
|
|||||||
floating_layer_behaviour: Default::default(),
|
floating_layer_behaviour: Default::default(),
|
||||||
globals: Default::default(),
|
globals: Default::default(),
|
||||||
workspace_config: None,
|
workspace_config: None,
|
||||||
|
locked_containers: Default::default(),
|
||||||
wallpaper: None,
|
wallpaper: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,16 +205,18 @@ impl Workspace {
|
|||||||
|
|
||||||
if let Some(layout) = &config.layout {
|
if let Some(layout) = &config.layout {
|
||||||
self.layout = Layout::Default(*layout);
|
self.layout = Layout::Default(*layout);
|
||||||
|
self.tile = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pathbuf) = &config.custom_layout {
|
if let Some(pathbuf) = &config.custom_layout {
|
||||||
let layout = CustomLayout::from_path(pathbuf)?;
|
let layout = CustomLayout::from_path(pathbuf)?;
|
||||||
self.layout = Layout::Custom(layout);
|
self.layout = Layout::Custom(layout);
|
||||||
|
self.tile = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tile =
|
if config.custom_layout.is_none() && config.layout.is_none() {
|
||||||
!(config.custom_layout.is_none() && config.layout.is_none() && config.tile.is_none()
|
self.tile = false;
|
||||||
|| config.tile.is_some_and(|tile| !tile));
|
}
|
||||||
|
|
||||||
let mut all_layout_rules = vec![];
|
let mut all_layout_rules = vec![];
|
||||||
if let Some(layout_rules) = &config.layout_rules {
|
if let Some(layout_rules) = &config.layout_rules {
|
||||||
@@ -242,8 +241,6 @@ impl Workspace {
|
|||||||
self.set_layout_rules(all_layout_rules);
|
self.set_layout_rules(all_layout_rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_work_area_offset(config.work_area_offset);
|
|
||||||
|
|
||||||
self.set_apply_window_based_work_area_offset(
|
self.set_apply_window_based_work_area_offset(
|
||||||
config.apply_window_based_work_area_offset.unwrap_or(true),
|
config.apply_window_based_work_area_offset.unwrap_or(true),
|
||||||
);
|
);
|
||||||
@@ -270,7 +267,6 @@ impl Workspace {
|
|||||||
self.set_layout_flip(config.layout_flip);
|
self.set_layout_flip(config.layout_flip);
|
||||||
self.set_floating_layer_behaviour(config.floating_layer_behaviour);
|
self.set_floating_layer_behaviour(config.floating_layer_behaviour);
|
||||||
self.set_wallpaper(config.wallpaper.clone());
|
self.set_wallpaper(config.wallpaper.clone());
|
||||||
self.set_layout_options(config.layout_options);
|
|
||||||
|
|
||||||
self.set_workspace_config(Some(config.clone()));
|
self.set_workspace_config(Some(config.clone()));
|
||||||
|
|
||||||
@@ -499,7 +495,7 @@ impl Workspace {
|
|||||||
let border_width = self.globals().border_width;
|
let border_width = self.globals().border_width;
|
||||||
let border_offset = self.globals().border_offset;
|
let border_offset = self.globals().border_offset;
|
||||||
let work_area = self.globals().work_area;
|
let work_area = self.globals().work_area;
|
||||||
let work_area_offset = self.work_area_offset().or(self.globals().work_area_offset);
|
let work_area_offset = self.globals().work_area_offset;
|
||||||
let window_based_work_area_offset = self.globals().window_based_work_area_offset;
|
let window_based_work_area_offset = self.globals().window_based_work_area_offset;
|
||||||
let window_based_work_area_offset_limit =
|
let window_based_work_area_offset_limit =
|
||||||
self.globals().window_based_work_area_offset_limit;
|
self.globals().window_based_work_area_offset_limit;
|
||||||
@@ -587,9 +583,6 @@ impl Workspace {
|
|||||||
Some(container_padding),
|
Some(container_padding),
|
||||||
self.layout_flip(),
|
self.layout_flip(),
|
||||||
self.resize_dimensions(),
|
self.resize_dimensions(),
|
||||||
self.focused_container_idx(),
|
|
||||||
self.layout_options(),
|
|
||||||
self.latest_layout(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||||
@@ -897,9 +890,10 @@ impl Workspace {
|
|||||||
// this fn respects locked container indexes - we should use it for pretty much everything
|
// this fn respects locked container indexes - we should use it for pretty much everything
|
||||||
// except monocle and maximize toggles
|
// except monocle and maximize toggles
|
||||||
pub fn insert_container_at_idx(&mut self, idx: usize, container: Container) -> usize {
|
pub fn insert_container_at_idx(&mut self, idx: usize, container: Container) -> usize {
|
||||||
let insertion_idx = self
|
let mut locked_containers = self.locked_containers().clone();
|
||||||
.containers_mut()
|
let mut ld = LockedDeque::new(self.containers_mut(), &mut locked_containers);
|
||||||
.insert_respecting_locks(idx, container);
|
let insertion_idx = ld.insert(idx, container);
|
||||||
|
self.locked_containers = locked_containers;
|
||||||
|
|
||||||
if insertion_idx > self.resize_dimensions().len() {
|
if insertion_idx > self.resize_dimensions().len() {
|
||||||
self.resize_dimensions_mut().push(None);
|
self.resize_dimensions_mut().push(None);
|
||||||
@@ -915,7 +909,10 @@ impl Workspace {
|
|||||||
// this fn respects locked container indexes - we should use it for pretty much everything
|
// this fn respects locked container indexes - we should use it for pretty much everything
|
||||||
// except monocle and maximize toggles
|
// except monocle and maximize toggles
|
||||||
pub fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
|
pub fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
|
||||||
let container = self.containers_mut().remove_respecting_locks(idx);
|
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;
|
||||||
|
|
||||||
if idx < self.resize_dimensions().len() {
|
if idx < self.resize_dimensions().len() {
|
||||||
self.resize_dimensions_mut().remove(idx);
|
self.resize_dimensions_mut().remove(idx);
|
||||||
@@ -1197,9 +1194,6 @@ impl Workspace {
|
|||||||
Layout::Default(DefaultLayout::UltrawideVerticalStack) => {
|
Layout::Default(DefaultLayout::UltrawideVerticalStack) => {
|
||||||
self.enforce_resize_for_ultrawide();
|
self.enforce_resize_for_ultrawide();
|
||||||
}
|
}
|
||||||
Layout::Default(DefaultLayout::Scrolling) => {
|
|
||||||
self.enforce_resize_for_scrolling();
|
|
||||||
}
|
|
||||||
_ => self.enforce_no_resize(),
|
_ => self.enforce_no_resize(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1427,28 +1421,6 @@ 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) {
|
fn enforce_no_resize(&mut self) {
|
||||||
for rect in self.resize_dimensions_mut().iter_mut().flatten() {
|
for rect in self.resize_dimensions_mut().iter_mut().flatten() {
|
||||||
rect.left = 0;
|
rect.left = 0;
|
||||||
@@ -1629,7 +1601,7 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn swap_containers(&mut self, i: usize, j: usize) {
|
pub fn swap_containers(&mut self, i: usize, j: usize) {
|
||||||
self.containers.elements_mut().swap_respecting_locks(i, j);
|
self.containers.swap(i, j);
|
||||||
self.focus_container(j);
|
self.focus_container(j);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1728,6 +1700,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::container::Container;
|
use crate::container::Container;
|
||||||
use crate::Window;
|
use crate::Window;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1735,18 +1708,20 @@ mod tests {
|
|||||||
let mut ws = Workspace::default();
|
let mut ws = Workspace::default();
|
||||||
|
|
||||||
let mut state = HashMap::new();
|
let mut state = HashMap::new();
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
|
||||||
// add 4 containers
|
// add 3 containers
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
let mut container = Container::default();
|
let container = Container::default();
|
||||||
if i == 3 {
|
|
||||||
container.set_locked(true); // set index 3 locked
|
|
||||||
}
|
|
||||||
state.insert(i, container.id().to_string());
|
state.insert(i, container.id().to_string());
|
||||||
ws.add_container_to_back(container);
|
ws.add_container_to_back(container);
|
||||||
}
|
}
|
||||||
assert_eq!(ws.containers().len(), 4);
|
assert_eq!(ws.containers().len(), 4);
|
||||||
|
|
||||||
|
// set index 3 locked
|
||||||
|
locked.insert(3);
|
||||||
|
ws.locked_containers = locked;
|
||||||
|
|
||||||
// focus container at index 2
|
// focus container at index 2
|
||||||
ws.focus_container(2);
|
ws.focus_container(2);
|
||||||
|
|
||||||
@@ -1778,17 +1753,20 @@ mod tests {
|
|||||||
fn test_locked_containers_remove_window() {
|
fn test_locked_containers_remove_window() {
|
||||||
let mut ws = Workspace::default();
|
let mut ws = Workspace::default();
|
||||||
|
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
|
||||||
// add 4 containers
|
// add 4 containers
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
let mut container = Container::default();
|
let mut container = Container::default();
|
||||||
container.windows_mut().push_back(Window::from(i));
|
container.windows_mut().push_back(Window::from(i));
|
||||||
if i == 1 {
|
|
||||||
container.set_locked(true);
|
|
||||||
}
|
|
||||||
ws.add_container_to_back(container);
|
ws.add_container_to_back(container);
|
||||||
}
|
}
|
||||||
assert_eq!(ws.containers().len(), 4);
|
assert_eq!(ws.containers().len(), 4);
|
||||||
|
|
||||||
|
// set index 1 locked
|
||||||
|
locked.insert(1);
|
||||||
|
ws.locked_containers = locked;
|
||||||
|
|
||||||
ws.remove_window(0).unwrap();
|
ws.remove_window(0).unwrap();
|
||||||
assert_eq!(ws.containers()[0].focused_window().unwrap().hwnd, 2);
|
assert_eq!(ws.containers()[0].focused_window().unwrap().hwnd, 2);
|
||||||
// index 1 should still be the same
|
// index 1 should still be the same
|
||||||
@@ -1800,17 +1778,20 @@ mod tests {
|
|||||||
fn test_locked_containers_toggle_float() {
|
fn test_locked_containers_toggle_float() {
|
||||||
let mut ws = Workspace::default();
|
let mut ws = Workspace::default();
|
||||||
|
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
|
||||||
// add 4 containers
|
// add 4 containers
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
let mut container = Container::default();
|
let mut container = Container::default();
|
||||||
container.windows_mut().push_back(Window::from(i));
|
container.windows_mut().push_back(Window::from(i));
|
||||||
if i == 1 {
|
|
||||||
container.set_locked(true);
|
|
||||||
}
|
|
||||||
ws.add_container_to_back(container);
|
ws.add_container_to_back(container);
|
||||||
}
|
}
|
||||||
assert_eq!(ws.containers().len(), 4);
|
assert_eq!(ws.containers().len(), 4);
|
||||||
|
|
||||||
|
// set index 1 locked
|
||||||
|
locked.insert(1);
|
||||||
|
ws.locked_containers = locked;
|
||||||
|
|
||||||
// set index 0 focused
|
// set index 0 focused
|
||||||
ws.focus_container(0);
|
ws.focus_container(0);
|
||||||
|
|
||||||
@@ -1842,17 +1823,20 @@ mod tests {
|
|||||||
fn test_locked_containers_stack() {
|
fn test_locked_containers_stack() {
|
||||||
let mut ws = Workspace::default();
|
let mut ws = Workspace::default();
|
||||||
|
|
||||||
|
let mut locked = BTreeSet::new();
|
||||||
|
|
||||||
// add 6 containers
|
// add 6 containers
|
||||||
for i in 0..6 {
|
for i in 0..6 {
|
||||||
let mut container = Container::default();
|
let mut container = Container::default();
|
||||||
container.windows_mut().push_back(Window::from(i));
|
container.windows_mut().push_back(Window::from(i));
|
||||||
if i == 4 {
|
|
||||||
container.set_locked(true);
|
|
||||||
}
|
|
||||||
ws.add_container_to_back(container);
|
ws.add_container_to_back(container);
|
||||||
}
|
}
|
||||||
assert_eq!(ws.containers().len(), 6);
|
assert_eq!(ws.containers().len(), 6);
|
||||||
|
|
||||||
|
// set index 4 locked
|
||||||
|
locked.insert(4);
|
||||||
|
ws.locked_containers = locked;
|
||||||
|
|
||||||
// set index 3 focused
|
// set index 3 focused
|
||||||
ws.focus_container(3);
|
ws.focus_container(3);
|
||||||
|
|
||||||
@@ -1955,33 +1939,6 @@ mod tests {
|
|||||||
assert_eq!(container.windows().len(), 3);
|
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]
|
#[test]
|
||||||
fn test_remove_focused_container() {
|
fn test_remove_focused_container() {
|
||||||
let mut workspace = Workspace::default();
|
let mut workspace = Workspace::default();
|
||||||
@@ -2307,25 +2264,6 @@ mod tests {
|
|||||||
assert_eq!(container.windows().len(), 1);
|
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]
|
#[test]
|
||||||
fn test_remove_window() {
|
fn test_remove_window() {
|
||||||
let mut workspace = Workspace::default();
|
let mut workspace = Workspace::default();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebic-no-console"
|
name = "komorebic-no-console"
|
||||||
version = "0.1.38"
|
version = "0.1.37"
|
||||||
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
||||||
repository = "https://github.com/LGUG2Z/komorebi"
|
repository = "https://github.com/LGUG2Z/komorebi"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -184,10 +184,6 @@ MonitorWorkAreaOffset(monitor, left, top, right, bottom) {
|
|||||||
RunWait("komorebic.exe monitor-work-area-offset " monitor " " left " " top " " right " " bottom, , "Hide")
|
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) {
|
AdjustContainerPadding(sizing, adjustment) {
|
||||||
RunWait("komorebic.exe adjust-container-padding " sizing " " adjustment, , "Hide")
|
RunWait("komorebic.exe adjust-container-padding " sizing " " adjustment, , "Hide")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebic"
|
name = "komorebic"
|
||||||
version = "0.1.38"
|
version = "0.1.37"
|
||||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||||
repository = "https://github.com/LGUG2Z/komorebi"
|
repository = "https://github.com/LGUG2Z/komorebi"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -8,7 +8,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
komorebi-client = { path = "../komorebi-client" }
|
||||||
|
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
@@ -36,7 +36,7 @@ shadow-rs = { workspace = true }
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["schemars"]
|
default = ["schemars"]
|
||||||
schemars = ["dep:schemars", "komorebi-client/default"]
|
schemars = ["dep:schemars", "komorebi-client/schemars"]
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(FALSE)'] }
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(FALSE)'] }
|
||||||
+10
-96
@@ -6,11 +6,9 @@ use komorebi_client::replace_env_in_path;
|
|||||||
use komorebi_client::PathExt;
|
use komorebi_client::PathExt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io;
|
|
||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::num::NonZeroUsize;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
@@ -93,7 +91,8 @@ lazy_static! {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
whkd_config_home.is_dir(),
|
whkd_config_home.is_dir(),
|
||||||
"$Env:WHKD_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
"$Env:WHKD_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||||
|
home_path
|
||||||
);
|
);
|
||||||
|
|
||||||
whkd_config_home
|
whkd_config_home
|
||||||
@@ -429,22 +428,6 @@ struct MonitorWorkAreaOffset {
|
|||||||
bottom: i32,
|
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)]
|
#[derive(Parser)]
|
||||||
struct MonitorIndexPreference {
|
struct MonitorIndexPreference {
|
||||||
/// Preferred monitor index (zero-indexed)
|
/// Preferred monitor index (zero-indexed)
|
||||||
@@ -980,12 +963,6 @@ struct EagerFocus {
|
|||||||
exe: String,
|
exe: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
struct ScrollingLayoutColumns {
|
|
||||||
/// Desired number of visible columns
|
|
||||||
count: NonZeroUsize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
||||||
struct Opts {
|
struct Opts {
|
||||||
@@ -1205,9 +1182,6 @@ enum SubCommand {
|
|||||||
/// Set offsets for a monitor to exclude parts of the work area from tiling
|
/// Set offsets for a monitor to exclude parts of the work area from tiling
|
||||||
#[clap(arg_required_else_help = true)]
|
#[clap(arg_required_else_help = true)]
|
||||||
MonitorWorkAreaOffset(MonitorWorkAreaOffset),
|
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
|
/// Toggle application of the window-based work area offset for the focused workspace
|
||||||
ToggleWindowBasedWorkAreaOffset,
|
ToggleWindowBasedWorkAreaOffset,
|
||||||
/// Set container padding on the focused workspace
|
/// Set container padding on the focused workspace
|
||||||
@@ -1228,9 +1202,6 @@ enum SubCommand {
|
|||||||
/// Cycle between available layouts
|
/// Cycle between available layouts
|
||||||
#[clap(arg_required_else_help = true)]
|
#[clap(arg_required_else_help = true)]
|
||||||
CycleLayout(CycleLayout),
|
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
|
/// Load a custom layout from file for the focused workspace
|
||||||
#[clap(hide = true)]
|
#[clap(hide = true)]
|
||||||
#[clap(arg_required_else_help = true)]
|
#[clap(arg_required_else_help = true)]
|
||||||
@@ -1327,7 +1298,7 @@ enum SubCommand {
|
|||||||
ToggleWorkspaceFloatOverride,
|
ToggleWorkspaceFloatOverride,
|
||||||
/// Toggle between the Tiling and Floating layers on the focused workspace
|
/// Toggle between the Tiling and Floating layers on the focused workspace
|
||||||
ToggleWorkspaceLayer,
|
ToggleWorkspaceLayer,
|
||||||
/// Toggle the paused state for all window tiling
|
/// Toggle window tiling on the focused workspace
|
||||||
TogglePause,
|
TogglePause,
|
||||||
/// Toggle window tiling on the focused workspace
|
/// Toggle window tiling on the focused workspace
|
||||||
ToggleTiling,
|
ToggleTiling,
|
||||||
@@ -1581,33 +1552,6 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SubCommand::Quickstart => {
|
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 local_appdata_dir = data_local_dir().expect("could not find localdata dir");
|
||||||
let data_dir = local_appdata_dir.join("komorebi");
|
let data_dir = local_appdata_dir.join("komorebi");
|
||||||
std::fs::create_dir_all(&*WHKD_CONFIG_DIR)?;
|
std::fs::create_dir_all(&*WHKD_CONFIG_DIR)?;
|
||||||
@@ -1623,30 +1567,17 @@ fn main() -> Result<()> {
|
|||||||
komorebi_json.replace("Env:USERPROFILE", "Env:KOMOREBI_CONFIG_HOME");
|
komorebi_json.replace("Env:USERPROFILE", "Env:KOMOREBI_CONFIG_HOME");
|
||||||
}
|
}
|
||||||
|
|
||||||
let komorebi_path = HOME_DIR.join("komorebi.json");
|
std::fs::write(HOME_DIR.join("komorebi.json"), komorebi_json)?;
|
||||||
let bar_path = HOME_DIR.join("komorebi.bar.json");
|
std::fs::write(HOME_DIR.join("komorebi.bar.json"), 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");
|
let applications_json = include_str!("../applications.json");
|
||||||
write_file_with_prompt(&applications_path, applications_json, &mut written_files)?;
|
std::fs::write(HOME_DIR.join("applications.json"), applications_json)?;
|
||||||
|
|
||||||
let whkdrc = include_str!("../../docs/whkdrc.sample");
|
let whkdrc = include_str!("../../docs/whkdrc.sample");
|
||||||
write_file_with_prompt(&whkdrc_path, whkdrc, &mut written_files)?;
|
std::fs::write(WHKD_CONFIG_DIR.join("whkdrc"), whkdrc)?;
|
||||||
if written_files.is_empty() {
|
|
||||||
println!("\nNo files were written.")
|
println!("Example komorebi.json, komorebi.bar.json, whkdrc and latest applications.json files created");
|
||||||
} else {
|
println!("You can now run komorebic start --whkd --bar");
|
||||||
println!(
|
|
||||||
"\nThe following example files were written:\n{}",
|
|
||||||
written_files.join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
println!("\nYou can now run komorebic start --whkd --bar");
|
|
||||||
}
|
}
|
||||||
SubCommand::EnableAutostart(args) => {
|
SubCommand::EnableAutostart(args) => {
|
||||||
let mut current_exe = std::env::current_exe().expect("unable to get exec path");
|
let mut current_exe = std::env::current_exe().expect("unable to get exec path");
|
||||||
@@ -1998,20 +1929,6 @@ fn main() -> Result<()> {
|
|||||||
bottom: arg.bottom,
|
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 => {
|
SubCommand::ToggleWindowBasedWorkAreaOffset => {
|
||||||
send_message(&SocketMessage::ToggleWindowBasedWorkAreaOffset)?;
|
send_message(&SocketMessage::ToggleWindowBasedWorkAreaOffset)?;
|
||||||
}
|
}
|
||||||
@@ -2708,9 +2625,6 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
|||||||
SubCommand::CycleLayout(arg) => {
|
SubCommand::CycleLayout(arg) => {
|
||||||
send_message(&SocketMessage::CycleLayout(arg.cycle_direction))?;
|
send_message(&SocketMessage::CycleLayout(arg.cycle_direction))?;
|
||||||
}
|
}
|
||||||
SubCommand::ScrollingLayoutColumns(arg) => {
|
|
||||||
send_message(&SocketMessage::ScrollingLayoutColumns(arg.count))?;
|
|
||||||
}
|
|
||||||
SubCommand::LoadCustomLayout(arg) => {
|
SubCommand::LoadCustomLayout(arg) => {
|
||||||
send_message(&SocketMessage::ChangeLayoutCustom(arg.path))?;
|
send_message(&SocketMessage::ChangeLayoutCustom(arg.path))?;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -80,6 +80,7 @@ nav:
|
|||||||
- common-workflows/tray-and-multi-window-applications.md
|
- common-workflows/tray-and-multi-window-applications.md
|
||||||
- common-workflows/mouse-follows-focus.md
|
- common-workflows/mouse-follows-focus.md
|
||||||
- common-workflows/dynamic-layout-switching.md
|
- common-workflows/dynamic-layout-switching.md
|
||||||
|
- common-workflows/set-display-index.md
|
||||||
- common-workflows/multiple-bar-instances.md
|
- common-workflows/multiple-bar-instances.md
|
||||||
- common-workflows/multi-monitor-setup.md
|
- common-workflows/multi-monitor-setup.md
|
||||||
- CLI reference:
|
- CLI reference:
|
||||||
@@ -95,7 +96,6 @@ nav:
|
|||||||
- cli/state.md
|
- cli/state.md
|
||||||
- cli/global-state.md
|
- cli/global-state.md
|
||||||
- cli/gui.md
|
- cli/gui.md
|
||||||
- cli/toggle-shortcuts.md
|
|
||||||
- cli/visible-windows.md
|
- cli/visible-windows.md
|
||||||
- cli/monitor-information.md
|
- cli/monitor-information.md
|
||||||
- cli/query.md
|
- cli/query.md
|
||||||
@@ -158,7 +158,6 @@ nav:
|
|||||||
- cli/invisible-borders.md
|
- cli/invisible-borders.md
|
||||||
- cli/global-work-area-offset.md
|
- cli/global-work-area-offset.md
|
||||||
- cli/monitor-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/toggle-window-based-work-area-offset.md
|
||||||
- cli/focused-workspace-container-padding.md
|
- cli/focused-workspace-container-padding.md
|
||||||
- cli/focused-workspace-padding.md
|
- cli/focused-workspace-padding.md
|
||||||
@@ -166,7 +165,6 @@ nav:
|
|||||||
- cli/adjust-workspace-padding.md
|
- cli/adjust-workspace-padding.md
|
||||||
- cli/change-layout.md
|
- cli/change-layout.md
|
||||||
- cli/cycle-layout.md
|
- cli/cycle-layout.md
|
||||||
- cli/scrolling-layout-columns.md
|
|
||||||
- cli/flip-layout.md
|
- cli/flip-layout.md
|
||||||
- cli/promote.md
|
- cli/promote.md
|
||||||
- cli/promote-focus.md
|
- cli/promote-focus.md
|
||||||
|
|||||||
+4
-55316
File diff suppressed because it is too large
Load Diff
+5
-64
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "StaticConfig",
|
"title": "StaticConfig",
|
||||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.38`",
|
"description": "The `komorebi.json` static configuration file reference for `v0.1.37`",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"animation": {
|
"animation": {
|
||||||
@@ -1790,8 +1790,7 @@
|
|||||||
"HorizontalStack",
|
"HorizontalStack",
|
||||||
"UltrawideVerticalStack",
|
"UltrawideVerticalStack",
|
||||||
"Grid",
|
"Grid",
|
||||||
"RightMainVerticalStack",
|
"RightMainVerticalStack"
|
||||||
"Scrolling"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"layout_flip": {
|
"layout_flip": {
|
||||||
@@ -1803,27 +1802,6 @@
|
|||||||
"HorizontalAndVertical"
|
"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": {
|
"layout_rules": {
|
||||||
"description": "Layout rules in the format of threshold => layout (default: None)",
|
"description": "Layout rules in the format of threshold => layout (default: None)",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1837,8 +1815,7 @@
|
|||||||
"HorizontalStack",
|
"HorizontalStack",
|
||||||
"UltrawideVerticalStack",
|
"UltrawideVerticalStack",
|
||||||
"Grid",
|
"Grid",
|
||||||
"RightMainVerticalStack",
|
"RightMainVerticalStack"
|
||||||
"Scrolling"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1846,10 +1823,6 @@
|
|||||||
"description": "Name",
|
"description": "Name",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"tile": {
|
|
||||||
"description": "Enable or disable tiling for the workspace (default: true)",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"wallpaper": {
|
"wallpaper": {
|
||||||
"description": "Specify a wallpaper for this workspace",
|
"description": "Specify a wallpaper for this workspace",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -2142,38 +2115,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"workspace_padding": {
|
||||||
"description": "Workspace padding (default: global)",
|
"description": "Workspace padding (default: global)",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -2557,7 +2498,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"description": "Stackbar mode (default: Never)",
|
"description": "Stackbar mode",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"Always",
|
"Always",
|
||||||
@@ -4686,7 +4627,7 @@
|
|||||||
"description": "Which Windows signal to use when hiding windows (default: Cloak)",
|
"description": "Which Windows signal to use when hiding windows (default: Cloak)",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"description": "END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)",
|
"description": "Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"Hide"
|
"Hide"
|
||||||
|
|||||||
Reference in New Issue
Block a user