Compare commits

..

44 Commits

Author SHA1 Message Date
LGUG2Z
ffa76ea28c chore(release): v0.1.38 2025-09-12 17:36:04 -07:00
Lucas de Linhares
2c00d79968 perf(cargo): add release-opt profile 2025-09-12 17:05:24 -07:00
LGUG2Z
78177af6b8 docs(mkdocs): run docgen, depgen and jsonschema targets 2025-09-09 08:56:29 -07:00
dependabot[bot]
86e0d40828 chore(deps): bump actions/github-script from 7 to 8
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 10:03:38 -07:00
LGUG2Z
48f6ac8964 chore(deps): cargo update 2025-09-08 08:17:40 -07:00
omark96
1db572f789 feat(wm): implement Clone for State, Notification and NotificationEvent
This implements Clone for State, Notification and NotificationEvent to allow for the use of these
types in Rhai scripts.
2025-09-02 07:53:36 -07:00
1337cookie
3dad77533b feat(bar): add config opts for ro and removable disks
This lets the user enable or disable showing removable or read only
disks in the komorebi-bar storage widget.

Removable disks are set to show by default.

Read-only disks are set not to show by default. Having windows sandbox
installed displays a very long read only disk which could be problematic
for new users.
2025-09-01 15:15:41 -07:00
omark96
f40fb9a251 feat(wm): add config option to set tiling
This commit adds the option to set whether a workspace should be tiled or not by default. It retains
the default behaviour of komorebi, but adds the option to set a workspace to not be tiled by
default, but still be able to change the default layout for that workspace.
2025-09-01 15:04:59 -07:00
omark96
b4e16e43e9 feat(cli): change quickstart to prompt user before writing files
This commit makes it so the quickstart command first checks for the existence of the config files.
If they don't exist it writes them, if they do exist it prompts the user to whether or not they want
to overwrite the existing files. Lastly it prints the full path to the files that were written. This
is to prevent users from accidentally overwriting their configs as well as making it clearer where
komorebi places the config files for new users.
2025-09-01 15:03:48 -07:00
LGUG2Z
4c2e8ff6d2 chore(deps): cargo update 2025-09-01 15:03:09 -07:00
Jerry Kingsbury
c879aae1e7 test(wm): monocle on and off on nonexistent container
Created a test that tests moving a nonexistent container to monocle and
retreiving a nonexistent container from monocle. The test ensures we
receive an error when attempting to use monocle_on or monocle_off when a
container doesn't exist.
2025-08-27 18:05:07 -07:00
Jerry Kingsbury
7e87e83189 test(wm): toggle monocle on nonexistent container
Created a test to test toggle monocle when a container doesn't exist in
the monitor. The test ensures that we receive an error when attempting
to call toggle monocle when a monitor doesn't contain a container.
2025-08-27 18:05:07 -07:00
Jerry Kingsbury
aae9338f66 test(wm): toggle maximize a nonexistent window
The test ensures that we receive an error when attempting to toggle
maximize a window that doesn't exist.
2025-08-27 18:05:07 -07:00
Jerry Kingsbury
f68a709f1d test(wm): float nonexistent window
Created a test to test trying to float a nonexistent window. The test
ensures that we receive an error when trying to float a window in an
empty container
2025-08-27 18:05:07 -07:00
Jerry Kingsbury
c76846ac63 test(wm): cycle windows in an empty container
Created a test to test cycling windows in an empty container. The test
ensures that when attempting to cycle windows in an empty container it
will push out an error.
2025-08-27 18:05:07 -07:00
Jerry Kingsbury
b29dd8b1d1 test(wm): remove nonexistent window from container
Created a test that tests removing a nonexistent window from a
container. The test creates a monitor containing an empty container and
attempts to remove a window from the container. The test ensures that
the result is an error when calling remove_window_from_container.
2025-08-27 18:05:07 -07:00
omark96
59c3c14731 feat(config): add work-area-offset per workspace
This commit adds the option to set 'work_area_offset' per workspace. If
no workspace work area offset is set for that workspace it will instead
use the value of the globals.work_area_offset for that workspace.

This commit adds a command to set the work area offset of a workspace
given a monitor index and a workspace index.
2025-08-27 18:05:07 -07:00
dependabot[bot]
db96f2cc5a chore(deps): bump reqwest from 0.12.22 to 0.12.23
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.22 to 0.12.23.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.22...v0.12.23)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-version: 0.12.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-27 18:05:07 -07:00
dependabot[bot]
a37a6752a8 chore(deps): bump clap from 4.5.41 to 4.5.45
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.41 to 4.5.45.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.41...clap_complete-v4.5.45)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-27 18:05:07 -07:00
loeiks
4d0df9c5b5 fix(cli): fix typo in the toggle-pause docs 2025-08-27 18:05:07 -07:00
dependabot[bot]
f8ea62f857 chore(deps): bump shadow-rs from 1.2.0 to 1.2.1
Bumps [shadow-rs](https://github.com/baoyachi/shadow-rs) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/baoyachi/shadow-rs/releases)
- [Commits](https://github.com/baoyachi/shadow-rs/compare/v1.2.0...v1.2.1)

---
updated-dependencies:
- dependency-name: shadow-rs
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-17 10:09:45 -07:00
dependabot[bot]
93bb41737b chore(deps): bump serde_json from 1.0.141 to 1.0.142
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.141 to 1.0.142.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.141...v1.0.142)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.142
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-17 10:09:35 -07:00
dependabot[bot]
280352eeef chore(deps): bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-17 10:06:45 -07:00
dependabot[bot]
7619b9b4ed chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-17 10:06:36 -07:00
dependabot[bot]
72a4d5276e chore(deps): bump slab from 0.4.10 to 0.4.11
Bumps [slab](https://github.com/tokio-rs/slab) from 0.4.10 to 0.4.11.
- [Release notes](https://github.com/tokio-rs/slab/releases)
- [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/slab/compare/v0.4.10...v0.4.11)

---
updated-dependencies:
- dependency-name: slab
  dependency-version: 0.4.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 07:07:54 -07:00
JustForFun88
4bb3b83d57 refactor(bar) split komorebi widget into smaller parts
This PR significantly refactors the komorebi  bar rendering logic,
simplifying state management, and addressing some found bugs. The
primary motivation was to make the codebase more readable and
maintainable.

Key Changes:

- Allocation Reduction: Removed most per-frame structure allocations.

- Runtime Matching Elimination: Replaced runtime pattern matching with
  pre-selected function pointers determined at initialization. Widget
  validations and configurations are now performed during widget
  creation rather than per-frame checks. For example, widget enablement
  is now handled by an Option that wraps each ..Bar structure. If a
  widget is enabled, its structure is present; otherwise, it is None.
  This eliminates the need for runtime enabled checks.

- Widget Modularity: Code is split into smaller parts, reducing
  complexity.

Bug Fixes:

- Corrected icon sizing for floating windows following regular
  containers, ensuring icons revert correctly from icon_size to
  text_size.

- There was also another bug with a floating window positioned above a
  monocle container, but I forgot the details 😅
2025-07-31 17:14:39 -07:00
omark96
ccd2f3a464 fix(wm): move last_focused_workspace logic to focus_workspace method
There are currently a number of commands that do not update the previously focused workspace
correctly. The previous method relied on handling each case, this change makes it so that
last_focused_workspace is always updated whenever the focus is changed.
2025-07-23 09:47:57 -07:00
dependabot[bot]
7e242ada66 chore(deps): bump netdev from 0.35.3 to 0.36.0
---
updated-dependencies:
- dependency-name: netdev
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 10:59:39 -07:00
dependabot[bot]
5b2acd0f12 chore(deps): bump shadow-rs from 1.1.1 to 1.2.0
Bumps [shadow-rs](https://github.com/baoyachi/shadow-rs) from 1.1.1 to 1.2.0.
- [Release notes](https://github.com/baoyachi/shadow-rs/releases)
- [Commits](https://github.com/baoyachi/shadow-rs/compare/v1.1.1...v1.2.0)

---
updated-dependencies:
- dependency-name: shadow-rs
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-20 19:14:59 -07:00
LGUG2Z
ec0bbaae98 chore(deps): cargo update 2025-07-20 18:49:37 -07:00
LGUG2Z
3c44f3bfdb refactor(clippy): apply lints 2025-07-19 16:42:25 -07:00
Martin Hjulstad Bednar
7839980ddf feat(shortcuts): show all hotkey bindings
- Removed command filtering logic from the shortcuts UI.
- All hotkey bindings are now shown regardless of command.
- Improved UI with hint text for the filter input.
2025-07-16 21:04:15 -07:00
JustForFun88
6416c0b6eb refactor(wm): move 'locked' flag down to containers
Key Changes
- Added `locked: bool` field directly to the `Container` struct.
- Removed `locked_containers` from `Workspace`.
- Updated `komorebi_bar` to access `locked` directly
  from `Container`.

Insert and swap operations respects `locked` container
indexes in the sequence
2025-06-15 12:30:03 -07:00
LGUG2Z
f6ccec9505 chore(deps): cargo update 2025-06-14 16:44:55 -07:00
LGUG2Z
21cb5e1e6f feat(stackbar): set title as default label
Following discussing in #1475, and considering the default of the
komorebi widget in komorebi-bar, this commit updates the default
StackbarLabel from Process to Title.

resolve #1475
2025-06-05 22:03:53 -07:00
LGUG2Z
009c0dcd28 chore(deps): cargo update 2025-06-04 15:54:15 -07:00
LGUG2Z
98c5ab3b9b fix(wm): prevent stack-all issues with n>1 stacks on ws
This commit ensures that unstack_all is called on a workspace before
attempting to proceed with stack_all to avoid stacking loops that can
occur when there are multiple pre-existing stacks already created on the
workspace.
2025-06-04 15:51:05 -07:00
dependabot[bot]
d4eeec994f chore(deps): bump netdev from 0.34.0 to 0.35.1
Bumps [netdev](https://github.com/shellrow/netdev) from 0.34.0 to 0.35.1.
- [Release notes](https://github.com/shellrow/netdev/releases)
- [Commits](https://github.com/shellrow/netdev/commits)

---
updated-dependencies:
- dependency-name: netdev
  dependency-version: 0.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 10:47:34 -07:00
dependabot[bot]
e9ed1cfd3b chore(deps): bump reqwest from 0.12.15 to 0.12.19
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.15 to 0.12.19.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.15...v0.12.19)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-version: 0.12.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 10:47:05 -07:00
dependabot[bot]
4a2eb391f7 chore(deps): bump clap from 4.5.38 to 4.5.39
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.38 to 4.5.39.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.38...clap_complete-v4.5.39)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.39
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 10:46:48 -07:00
LGUG2Z
41e18bccc6 fix(stackbar): show regular cursor on hover
This commit ensures that the WM_SETCURSOR message is handled by stackbar
windows by setting the cursor to the default IDC_ARROW, which prevents
the spinning "Loading" icon from showing on hover.

re #1456
2025-05-27 08:12:58 -07:00
LGUG2Z
3d373b3630 fix(schema): flatten bar mouse config opts 2025-05-26 09:52:14 -07:00
LGUG2Z
b4e61b079c feat(wm): add scrolling layout
This commit adds a new DefaultLayout::Scrolling variant, along with a
new LayoutOptions configuration which will initially be used to allow
the user to declaratively specify the number of visible columns for the
Scrolling layout, and a new komorebic "scrolling-layout-columns" command
to allow the user to modify this value for the focused workspace at
runtime.

The Scrolling layout is inspired by the Niri scrolling window manager,
presenting a workspace as an infinite scrollable horizontal strip with a
viewport which includes the focused window + N other windows in columns.
There is no support for splitting columns into multiple rows.

This layout can currently only be applied to single-monitor setups as
the scrolling would result in layout calculations which push the windows
in the columns moving out of the viewport onto adjacent monitors.

This implementation in the current state is enough to be useable for me
personally, but if others want to iterate on this, make it handle
hiding/restoring windows correctly when scrolling the viewport so that
adjacent monitors don't get impacted etc., patches are always welcome.

resolve #1434
2025-05-17 21:12:51 -07:00
LGUG2Z
eec6312a51 chore(dev): begin v0.1.38-dev 2025-05-17 20:26:13 -07:00
67 changed files with 55803 additions and 54056 deletions

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Check and close feature issues
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const issue = context.payload.issue;

View File

@@ -21,7 +21,7 @@ jobs:
cargo-deny:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: EmbarkStudios/cargo-deny-action@v2
@@ -43,7 +43,7 @@ jobs:
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- run: rustup toolchain install stable --profile minimal
@@ -81,12 +81,12 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- shell: bash
run: echo "VERSION=nightly" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
@@ -128,14 +128,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
@@ -170,14 +170,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.ref_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi

1794
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ members = [
"komorebic-no-console",
"komorebi-bar",
"komorebi-themes",
"komorebi-shortcuts"
"komorebi-shortcuts",
]
[workspace.dependencies]
@@ -52,7 +52,7 @@ features = [
"Win32_Devices",
"Win32_Devices_Display",
"Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
@@ -73,5 +73,12 @@ features = [
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming",
"Media",
"Media_Control"
"Media_Control",
]
[profile.release-opt]
inherits = "release"
lto = true
panic = "abort"
codegen-units = 1
strip = true

View File

@@ -394,7 +394,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
```rust
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.37"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.38"}
use anyhow::Result;
use komorebi_client::Notification;

View File

@@ -14,7 +14,8 @@ feature-depth = 1
ignore = [
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" }
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" }
]
[licenses]

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
Arguments:
<DEFAULT_LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
Options:
-h, --help

View File

@@ -13,7 +13,7 @@ Arguments:
The number of window containers on-screen required to trigger this layout rule
<LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
Options:
-h, --help

View File

@@ -10,7 +10,7 @@ Arguments:
Target workspace name
<VALUE>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
Options:
-h, --help

View File

@@ -0,0 +1,16 @@
# scrolling-layout-columns
```
Set the number of visible columns for the Scrolling layout on the focused workspace
Usage: komorebic.exe scrolling-layout-columns <COUNT>
Arguments:
<COUNT>
Desired number of visible columns
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# toggle-pause
```
Toggle window tiling on the focused workspace
Toggle the paused state for all window tiling
Usage: komorebic.exe toggle-pause

View File

@@ -16,7 +16,7 @@ Arguments:
The number of window containers on-screen required to trigger this layout rule
<LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
Options:
-h, --help

View File

@@ -13,7 +13,7 @@ Arguments:
Workspace index on the specified monitor (zero-indexed)
<VALUE>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
Options:
-h, --help

View File

@@ -0,0 +1,31 @@
# workspace-work-area-offset
```
Set offsets for a workspace to exclude parts of the work area from tiling
Usage: komorebic.exe workspace-work-area-offset <MONITOR> <WORKSPACE> <LEFT> <TOP> <RIGHT> <BOTTOM>
Arguments:
<MONITOR>
Monitor index (zero-indexed)
<WORKSPACE>
Workspace index (zero-indexed)
<LEFT>
Size of the left work area offset (set right to left * 2 to maintain right padding)
<TOP>
Size of the top work area offset (set bottom to the same value to maintain bottom padding)
<RIGHT>
Size of the right work area offset
<BOTTOM>
Size of the bottom work area offset
Options:
-h, --help
Print help
```

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.37/schema.bar.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.38/schema.bar.json",
"monitor": 0,
"font_family": "JetBrains Mono",
"theme": {

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.37/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.38/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-bar"
version = "0.1.37"
version = "0.1.38"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -22,7 +22,7 @@ font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
lazy_static = { workspace = true }
netdev = "0.34"
netdev = "0.36"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"

View File

@@ -10,7 +10,7 @@ use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::KomorebiNotificationState;
use crate::widgets::komorebi::MonitorInfo;
use crate::widgets::widget::BarWidget;
use crate::widgets::widget::WidgetConfig;
use crate::KomorebiEvent;
@@ -47,17 +47,14 @@ use eframe::egui::Visuals;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::Colour;
use komorebi_client::KomorebiTheme;
use komorebi_client::MonitorNotification;
use komorebi_client::NotificationEvent;
use komorebi_client::PathExt;
use komorebi_client::SocketMessage;
use komorebi_client::VirtualDesktopNotification;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
use komorebi_themes::Base16Wrapper;
use komorebi_themes::Catppuccin;
use komorebi_themes::CatppuccinValue;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::cell::RefCell;
@@ -128,7 +125,7 @@ fn stop_powershell() -> Result<()> {
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) {
if let Err(e) = writeln!(session_stdin, "{cmd}") {
tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin");
return Err(e);
}
@@ -153,7 +150,7 @@ pub struct Komobar {
pub disabled: bool,
pub config: KomobarConfig,
pub render_config: Rc<RefCell<RenderConfig>>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
pub center_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>,
@@ -345,7 +342,7 @@ impl Komobar {
pub fn apply_config(
&mut self,
ctx: &Context,
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
previous_monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
) {
MAX_LABEL_WIDTH.store(
self.config.max_label_width.unwrap_or(400.0) as i32,
@@ -374,7 +371,7 @@ impl Komobar {
self.config.icon_scale,
));
let mut komorebi_notification_state = previous_notification_state;
let mut monitor_info = previous_monitor_info;
let mut komorebi_widgets = Vec::new();
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
@@ -426,19 +423,18 @@ impl Komobar {
komorebi_widgets
.into_iter()
.for_each(|(mut widget, idx, side)| {
match komorebi_notification_state {
match monitor_info {
None => {
komorebi_notification_state =
Some(widget.komorebi_notification_state.clone());
monitor_info = Some(widget.monitor_info.clone());
}
Some(ref previous) => {
if widget.workspaces.is_some_and(|w| w.enable) {
previous.borrow_mut().update_from_config(
&widget.komorebi_notification_state.borrow(),
);
if widget.workspaces.is_some() {
previous
.borrow_mut()
.update_from_self(&widget.monitor_info.borrow());
}
widget.komorebi_notification_state = previous.clone();
widget.monitor_info = previous.clone();
}
}
@@ -464,17 +460,17 @@ impl Komobar {
MonitorConfigOrIndex::Index(idx) => (*idx, None),
};
let mapped_state = self.komorebi_notification_state.as_ref().map(|state| {
let state = state.borrow();
let mapped_info = self.monitor_info.as_ref().map(|info| {
let monitor = info.borrow();
(
state.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
state.mouse_follows_focus,
monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
monitor.mouse_follows_focus,
)
});
if let Some(state) = mapped_state {
self.monitor_index = state.0;
self.mouse_follows_focus = state.1;
if let Some(info) = mapped_info {
self.monitor_index = info.0;
self.mouse_follows_focus = info.1;
}
if let Some(monitor_index) = self.monitor_index {
@@ -526,7 +522,7 @@ impl Komobar {
}
}
}
} else if self.komorebi_notification_state.is_some() && !self.disabled {
} else if self.monitor_info.is_some() && !self.disabled {
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
self.disabled = true;
} else {
@@ -566,7 +562,7 @@ impl Komobar {
tracing::info!("widget configuration options applied");
self.komorebi_notification_state = komorebi_notification_state;
self.monitor_info = monitor_info;
}
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
@@ -635,8 +631,7 @@ impl Komobar {
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
);
home
@@ -650,26 +645,6 @@ impl Komobar {
match komorebi_client::StaticConfig::read(&config) {
Ok(config) => {
if let Some(theme) = config.theme {
let stack_accent = match theme {
KomorebiTheme::Catppuccin {
name, stack_border, ..
} => stack_border
.unwrap_or(CatppuccinValue::Green)
.color32(name.as_theme()),
KomorebiTheme::Base16 {
name, stack_border, ..
} => stack_border
.unwrap_or(Base16Value::Base0B)
.color32(Base16Wrapper::Base16(name)),
KomorebiTheme::Custom {
ref colours,
stack_border,
..
} => stack_border
.unwrap_or(Base16Value::Base0B)
.color32(Base16Wrapper::Custom(colours.clone())),
};
apply_theme(
ctx,
KomobarTheme::from(theme),
@@ -679,10 +654,6 @@ impl Komobar {
bar_grouping,
self.render_config.clone(),
);
if let Some(state) = &self.komorebi_notification_state {
state.borrow_mut().stack_accent = Some(stack_accent);
}
}
}
Err(_) => {
@@ -725,7 +696,7 @@ impl Komobar {
disabled: false,
config,
render_config: Rc::new(RefCell::new(RenderConfig::new())),
komorebi_notification_state: None,
monitor_info: None,
left_widgets: vec![],
center_widgets: vec![],
right_widgets: vec![],
@@ -871,12 +842,12 @@ impl eframe::App for Komobar {
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
self.apply_config(ctx, self.komorebi_notification_state.clone());
self.apply_config(ctx, self.monitor_info.clone());
}
if let Ok(updated_config) = self.rx_config.try_recv() {
self.config = updated_config;
self.apply_config(ctx, self.komorebi_notification_state.clone());
self.apply_config(ctx, self.monitor_info.clone());
}
match self.rx_gui.try_recv() {
@@ -1001,24 +972,26 @@ impl eframe::App for Komobar {
}
}
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
komorebi_notification_state
.borrow_mut()
.handle_notification(
ctx,
self.monitor_index,
notification,
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
self.config.grouping,
self.config.theme.clone(),
self.render_config.clone(),
);
if let Some(monitor_info) = &self.monitor_info {
monitor_info.borrow_mut().update(
self.monitor_index,
notification.state,
self.render_config.borrow().show_all_icons,
);
handle_notification(
ctx,
notification.event,
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
self.config.grouping,
self.config.theme.clone(),
self.render_config.clone(),
);
}
if should_apply_config {
self.apply_config(ctx, self.komorebi_notification_state.clone());
self.apply_config(ctx, self.monitor_info.clone());
// Reposition the Bar
self.position_bar();
@@ -1344,3 +1317,64 @@ pub enum Alignment {
Center,
Right,
}
#[allow(clippy::too_many_arguments)]
fn handle_notification(
ctx: &Context,
event: komorebi_client::NotificationEvent,
bg_color: Rc<RefCell<Color32>>,
bg_color_with_alpha: Rc<RefCell<Color32>>,
transparency_alpha: Option<u8>,
grouping: Option<Grouping>,
default_theme: Option<KomobarTheme>,
render_config: Rc<RefCell<RenderConfig>>,
) {
if let NotificationEvent::Socket(message) = event {
match message {
SocketMessage::ReloadStaticConfiguration(path) => {
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from updated komorebi.json");
} else if let Some(default_theme) = default_theme {
apply_theme(
ctx,
default_theme,
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!(
"removed theme from updated komorebi.json and applied default theme"
);
} else {
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
}
}
}
SocketMessage::Theme(theme) => {
apply_theme(
ctx,
KomobarTheme::from(*theme),
bg_color,
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from komorebi socket message");
}
_ => {}
}
}
}

View File

@@ -16,7 +16,7 @@ use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// The `komorebi.bar.json` configuration file reference for `v0.1.37`
/// The `komorebi.bar.json` configuration file reference for `v0.1.38`
pub struct KomobarConfig {
/// Bar height (default: 50)
pub height: Option<f32>,
@@ -332,6 +332,7 @@ 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:
@@ -367,12 +368,10 @@ pub enum MouseMessage {
/// }
/// }
/// ```
#[serde(untagged)]
Komorebi(KomorebiMouseMessage),
/// Execute a custom command.
/// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved.
/// Example: `komorebic toggle-pause`
#[serde(untagged)]
Command(String),
}

View File

@@ -159,8 +159,7 @@ fn main() -> color_eyre::Result<()> {
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
);
home

View File

@@ -181,7 +181,7 @@ impl BarWidget for Battery {
.args(["/C", "start", "ms-settings:batterysaver"])
.spawn()
{
eprintln!("{}", error)
eprintln!("{error}")
}
}
});

View File

@@ -76,8 +76,8 @@ impl Cpu {
CpuOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {used}%"),
LabelPrefix::None | LabelPrefix::Icon => format!("{used}%"),
},
selected,
}
@@ -124,7 +124,7 @@ impl BarWidget for Cpu {
if let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
eprintln!("{error}")
}
}
});

View File

@@ -166,7 +166,7 @@ impl Date {
.to_string()
.trim()
.to_string(),
Err(_) => format!("Invalid timezone: {}", timezone),
Err(_) => format!("Invalid timezone: {timezone}"),
},
None => Local::now()
.format(&self.format.fmt_string())

File diff suppressed because it is too large Load Diff

View File

@@ -41,8 +41,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
let s: String = String::deserialize(deserializer)?;
// Attempt to deserialize the string as a DefaultLayout
if let Ok(default_layout) =
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
if let Ok(default_layout) = from_str::<komorebi_client::DefaultLayout>(&format!("\"{s}\""))
{
return Ok(KomorebiLayout::Default(default_layout));
}
@@ -53,7 +52,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
"Floating" => Ok(KomorebiLayout::Floating),
"Paused" => Ok(KomorebiLayout::Paused),
"Custom" => Ok(KomorebiLayout::Custom),
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
}
}
}
@@ -188,6 +187,12 @@ impl KomorebiLayout {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
}
// TODO: @CtByte can you think of a nice icon to draw here?
komorebi_client::DefaultLayout::Scrolling => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
},
KomorebiLayout::Monocle => {}
KomorebiLayout::Floating => {

View File

@@ -79,9 +79,9 @@ impl Memory {
MemoryOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {}%", usage)
format!("RAM: {usage}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
LabelPrefix::None | LabelPrefix::Icon => format!("{usage}%"),
},
selected,
}
@@ -128,7 +128,7 @@ impl BarWidget for Memory {
if let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
eprintln!("{error}")
}
}
});

View File

@@ -314,7 +314,7 @@ impl Network {
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error);
eprintln!("{error}");
}
}
}
@@ -535,6 +535,6 @@ enum DataUnit {
impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
write!(f, "{self:?}")
}
}

View File

@@ -25,6 +25,10 @@ pub struct StorageConfig {
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Show disks that are read only. (default: false)
pub show_read_only_disks: Option<bool>,
/// Show removable disks. (default: true)
pub show_removable_disks: Option<bool>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
/// Hide when the current percentage is under this value [[1-100]]
@@ -38,6 +42,8 @@ impl From<StorageConfig> for Storage {
disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
show_removable_disks: value.show_removable_disks.unwrap_or(true),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
last_updated: Instant::now(),
@@ -55,6 +61,8 @@ pub struct Storage {
disks: Disks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
show_read_only_disks: bool,
show_removable_disks: bool,
auto_select_over: Option<u8>,
auto_hide_under: Option<u8>,
last_updated: Instant,
@@ -71,6 +79,12 @@ impl Storage {
let mut disks = vec![];
for disk in &self.disks {
if disk.is_read_only() && !self.show_read_only_disks {
continue;
}
if disk.is_removable() && !self.show_removable_disks {
continue;
}
let mount = disk.mount_point();
let total = disk.total_space();
let available = disk.available_space();
@@ -87,7 +101,7 @@ impl Storage {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), percentage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
},
selected,
})
@@ -151,7 +165,7 @@ impl BarWidget for Storage {
])
.spawn()
{
eprintln!("{}", error)
eprintln!("{error}")
}
}
});

View File

@@ -209,7 +209,7 @@ impl Time {
Some(dt.time()),
)
}
Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
Err(_) => (format!("Invalid timezone: {timezone:?}"), None),
},
None => {
let dt = Local::now();

View File

@@ -148,7 +148,7 @@ impl BarWidget for Update {
)])
.spawn()
{
eprintln!("{}", error)
eprintln!("{error}")
}
}
});

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-client"
version = "0.1.37"
version = "0.1.38"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -77,6 +77,7 @@ pub use komorebi::WorkspaceConfig;
use komorebi::DATA_DIR;
use std::borrow::Borrow;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
@@ -94,12 +95,15 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
stream.write_all(serde_json::to_string(message)?.as_bytes())
}
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()>
where
Q: Borrow<SocketMessage>,
{
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
if let Ok(m_str) = serde_json::to_string(&m) {
if let Ok(m_str) = serde_json::to_string(m.borrow()) {
s.push_str(&m_str);
s.push('\n');
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-gui"
version = "0.1.37"
version = "0.1.38"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,5 @@
use eframe::egui::ViewportBuilder;
use std::path::PathBuf;
use whkd_core::HotkeyBinding;
use whkd_core::Whkdrc;
#[derive(Default)]
@@ -58,21 +57,18 @@ impl eframe::App for Quicklook {
ui.label("Filter");
ui.add(
eframe::egui::text_edit::TextEdit::singleline(&mut self.filter)
.hint_text("Filter by command...")
.background_color(ctx.style().visuals.faint_bg_color),
);
ui.end_row();
ui.end_row();
for binding in &whkdrc.bindings {
if is_komorebic_binding(binding) {
let keys = binding.keys.join(" + ");
if self.filter.is_empty()
|| binding.command.contains(&self.filter)
{
ui.label(keys);
ui.label(&binding.command);
ui.end_row();
}
let keys = binding.keys.join(" + ");
if self.filter.is_empty() || binding.command.contains(&self.filter)
{
ui.label(keys);
ui.label(&binding.command);
ui.end_row();
}
}
}
@@ -100,7 +96,3 @@ fn main() {
)
.unwrap();
}
fn is_komorebic_binding(binding: &HotkeyBinding) -> bool {
binding.command.starts_with("komorebic")
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-themes"
version = "0.1.37"
version = "0.1.38"
edition = "2021"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.37"
version = "0.1.38"
description = "A tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"

View File

@@ -17,5 +17,5 @@ pub enum AnimationPrefix {
}
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
format!("{}:{}", prefix, key)
format!("{prefix}:{key}")
}

View File

@@ -392,7 +392,7 @@ impl Border {
tracing::error!("failed to update border position {error}");
}
if !rect.is_same_size_as(&old_rect) {
if !rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect) {
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;

View File

@@ -255,7 +255,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let window_kind = if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
{
if ws.locked_containers().contains(&idx) {
if c.locked() {
WindowKind::UnfocusedLocked
} else {
WindowKind::Unfocused
@@ -356,7 +356,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
};
if !should_process_notification {
tracing::trace!("monitor state matches latest snapshot, skipping notification");
tracing::debug!("monitor state matches latest snapshot, skipping notification");
continue 'receiver;
}
@@ -563,7 +563,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|| monitor_idx != focused_monitor_idx
|| focused_window_hwnd != foreground_window
{
if ws.locked_containers().contains(&idx) {
if c.locked() {
WindowKind::UnfocusedLocked
} else {
WindowKind::Unfocused

View File

@@ -1,18 +1,24 @@
use std::collections::VecDeque;
use getset::CopyGetters;
use getset::Getters;
use getset::Setters;
use nanoid::nanoid;
use serde::Deserialize;
use serde::Serialize;
use crate::ring::Ring;
use crate::window::Window;
use crate::Lockable;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, CopyGetters, Setters)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Container {
#[getset(get = "pub")]
id: String,
#[serde(default)]
#[getset(get_copy = "pub", set = "pub")]
locked: bool,
windows: Ring<Window>,
}
@@ -22,11 +28,23 @@ impl Default for Container {
fn default() -> Self {
Self {
id: nanoid!(),
locked: false,
windows: Ring::default(),
}
}
}
impl Lockable for Container {
fn locked(&self) -> bool {
self.locked
}
fn set_locked(&mut self, locked: bool) -> &mut Self {
self.locked = locked;
self
}
}
impl Container {
pub fn hide(&self, omit: Option<isize>) {
for window in self.windows().iter().rev() {
@@ -144,6 +162,7 @@ impl Container {
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
#[test]
fn test_contains_window() {
@@ -250,4 +269,40 @@ mod tests {
// Should return None since window 4 doesn't exist
assert_eq!(container.idx_for_window(4), None);
}
#[test]
fn deserializes_with_missing_locked_field_defaults_to_false() {
let json = r#"{
"id": "test-1",
"windows": { "elements": [], "focused": 0 }
}"#;
let container: Container = serde_json::from_str(json).expect("Should deserialize");
assert!(!container.locked());
assert_eq!(container.id(), "test-1");
assert!(container.windows().is_empty());
let json = r#"{
"id": "test-2",
"windows": { "elements": [ { "hwnd": 5 }, { "hwnd": 9 } ], "focused": 1 }
}"#;
let container: Container = serde_json::from_str(json).unwrap();
assert_eq!(container.id(), "test-2");
assert!(!container.locked());
assert_eq!(container.windows(), &[Window::from(5), Window::from(9)]);
assert_eq!(container.focused_window_idx(), 1);
}
#[test]
fn serializes_and_deserializes() {
let mut container = Container::default();
container.set_locked(true);
let serialized = serde_json::to_string(&container).expect("Should serialize");
let deserialized: Container =
serde_json::from_str(&serialized).expect("Should deserialize");
assert!(deserialized.locked());
assert_eq!(deserialized.id(), container.id());
}
}

View File

@@ -12,8 +12,10 @@ use super::custom_layout::ColumnSplitWithCapacity;
use super::CustomLayout;
use super::DefaultLayout;
use super::Rect;
use crate::default_layout::LayoutOptions;
pub trait Arrangement {
#[allow(clippy::too_many_arguments)]
fn calculate(
&self,
area: &Rect,
@@ -21,6 +23,9 @@ pub trait Arrangement {
container_padding: Option<i32>,
layout_flip: Option<Axis>,
resize_dimensions: &[Option<Rect>],
focused_idx: usize,
layout_options: Option<LayoutOptions>,
latest_layout: &[Rect],
) -> Vec<Rect>;
}
@@ -33,9 +38,110 @@ impl Arrangement for DefaultLayout {
container_padding: Option<i32>,
layout_flip: Option<Axis>,
resize_dimensions: &[Option<Rect>],
focused_idx: usize,
layout_options: Option<LayoutOptions>,
latest_layout: &[Rect],
) -> Vec<Rect> {
let len = usize::from(len);
let mut dimensions = match self {
Self::Scrolling => {
let column_count = layout_options
.and_then(|o| o.scrolling.map(|s| s.columns))
.unwrap_or(3);
let column_width = area.right / column_count as i32;
let mut layouts = Vec::with_capacity(len);
match len {
// treat < 3 windows the same as the columns layout
len if len < 3 => {
layouts = columns(area, len);
let adjustment = calculate_columns_adjustment(resize_dimensions);
layouts.iter_mut().zip(adjustment.iter()).for_each(
|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
},
);
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
columns_reverse(&mut layouts);
}
}
}
// treat >= column_count as scrolling
len => {
let visible_columns = area.right / column_width;
let first_visible: isize = if focused_idx == 0 {
// if focused idx is 0, we are at the beginning of the scrolling strip
0
} else {
let previous_first_visible = if latest_layout.is_empty() {
0
} else {
// previous first_visible based on the left position of the first visible window
let left_edge = area.left;
latest_layout
.iter()
.position(|rect| rect.left >= left_edge)
.unwrap_or(0) as isize
};
let focused_idx = focused_idx as isize;
if focused_idx < previous_first_visible {
// focused window is off the left edge, we need to scroll left
focused_idx
} else if focused_idx
>= previous_first_visible + visible_columns as isize
{
// focused window is off the right edge, we need to scroll right
// and make sure it's the last visible window
(focused_idx + 1 - visible_columns as isize).max(0)
} else {
// focused window is already visible, we don't need to scroll
previous_first_visible
}
.min(
(len as isize)
.saturating_sub(visible_columns as isize)
.max(0),
)
};
for i in 0..len {
let position = (i as isize) - first_visible;
let left = area.left + (position as i32 * column_width);
layouts.push(Rect {
left,
top: area.top,
right: column_width,
bottom: area.bottom,
});
}
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
layouts.iter_mut().zip(adjustment.iter()).for_each(
|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
},
);
}
}
layouts
}
Self::BSP => recursive_fibonacci(
0,
len,
@@ -487,6 +593,9 @@ impl Arrangement for CustomLayout {
container_padding: Option<i32>,
_layout_flip: Option<Axis>,
_resize_dimensions: &[Option<Rect>],
_focused_idx: usize,
_layout_options: Option<LayoutOptions>,
_latest_layout: &[Rect],
) -> Vec<Rect> {
let mut dimensions = vec![];
let container_count = len.get();
@@ -541,7 +650,7 @@ impl Arrangement for CustomLayout {
};
match column {
Column::Primary(Option::Some(_)) => {
Column::Primary(Some(_)) => {
let main_column_area = if idx == 0 {
Self::main_column_area(area, primary_right, None)
} else {
@@ -1115,6 +1224,37 @@ fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rec
result
}
fn calculate_scrolling_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
if len <= 1 {
return result;
}
for (i, rect) in resize_dimensions.iter().enumerate() {
if let Some(rect) = rect {
let is_leftmost = i == 0;
let is_rightmost = i == len - 1;
resize_left(&mut result[i], rect.left);
resize_right(&mut result[i], rect.right);
resize_top(&mut result[i], rect.top);
resize_bottom(&mut result[i], rect.bottom);
if !is_leftmost && rect.left != 0 {
resize_right(&mut result[i - 1], rect.left);
}
if !is_rightmost && rect.right != 0 {
resize_left(&mut result[i + 1], rect.right);
}
}
}
result
}
fn resize_left(rect: &mut Rect, resize: i32) {
rect.left += resize / 2;
rect.right += -resize / 2;

View File

@@ -21,9 +21,24 @@ pub enum DefaultLayout {
UltrawideVerticalStack,
Grid,
RightMainVerticalStack,
Scrolling,
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LayoutOptions {
/// Options related to the Scrolling layout
pub scrolling: Option<ScrollingLayoutOptions>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ScrollingLayoutOptions {
/// Desired number of visible columns (default: 3)
pub columns: usize,
}
impl DefaultLayout {
pub fn leftmost_index(&self, len: usize) -> usize {
match self {
@@ -31,6 +46,7 @@ impl DefaultLayout {
n if n > 1 => 1,
_ => 0,
},
Self::Scrolling => 0,
DefaultLayout::BSP
| DefaultLayout::Columns
| DefaultLayout::Rows
@@ -53,6 +69,7 @@ impl DefaultLayout {
_ => len.saturating_sub(1),
},
DefaultLayout::RightMainVerticalStack => 0,
DefaultLayout::Scrolling => len.saturating_sub(1),
}
}
@@ -75,6 +92,7 @@ impl DefaultLayout {
| Self::RightMainVerticalStack
| Self::HorizontalStack
| Self::UltrawideVerticalStack
| Self::Scrolling
) {
return None;
};
@@ -169,13 +187,15 @@ impl DefaultLayout {
Self::HorizontalStack => Self::UltrawideVerticalStack,
Self::UltrawideVerticalStack => Self::Grid,
Self::Grid => Self::RightMainVerticalStack,
Self::RightMainVerticalStack => Self::BSP,
Self::RightMainVerticalStack => Self::Scrolling,
Self::Scrolling => Self::BSP,
}
}
#[must_use]
pub const fn cycle_previous(self) -> Self {
match self {
Self::Scrolling => Self::RightMainVerticalStack,
Self::RightMainVerticalStack => Self::Grid,
Self::Grid => Self::UltrawideVerticalStack,
Self::UltrawideVerticalStack => Self::HorizontalStack,

View File

@@ -102,6 +102,7 @@ impl Direction for DefaultLayout {
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx > 2,
Self::Grid => !is_grid_edge(op_direction, idx, count),
Self::Scrolling => false,
},
OperationDirection::Down => match self {
Self::BSP => idx != count - 1 && idx % 2 != 0,
@@ -111,6 +112,7 @@ impl Direction for DefaultLayout {
Self::HorizontalStack => idx == 0,
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
Self::Grid => !is_grid_edge(op_direction, idx, count),
Self::Scrolling => false,
},
OperationDirection::Left => match self {
Self::BSP => idx != 0,
@@ -120,6 +122,7 @@ impl Direction for DefaultLayout {
Self::HorizontalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx != 1,
Self::Grid => !is_grid_edge(op_direction, idx, count),
Self::Scrolling => idx != 0,
},
OperationDirection::Right => match self {
Self::BSP => idx % 2 == 0 && idx != count - 1,
@@ -133,6 +136,7 @@ impl Direction for DefaultLayout {
_ => idx < 2,
},
Self::Grid => !is_grid_edge(op_direction, idx, count),
Self::Scrolling => idx != count - 1,
},
}
}
@@ -158,6 +162,7 @@ impl Direction for DefaultLayout {
| Self::RightMainVerticalStack => idx - 1,
Self::HorizontalStack => 0,
Self::Grid => grid_neighbor(op_direction, idx, count),
Self::Scrolling => unreachable!(),
}
}
@@ -176,6 +181,7 @@ impl Direction for DefaultLayout {
Self::Columns => unreachable!(),
Self::HorizontalStack => 1,
Self::Grid => grid_neighbor(op_direction, idx, count),
Self::Scrolling => unreachable!(),
}
}
@@ -203,6 +209,7 @@ impl Direction for DefaultLayout {
_ => 0,
},
Self::Grid => grid_neighbor(op_direction, idx, count),
Self::Scrolling => idx - 1,
}
}
@@ -223,6 +230,7 @@ impl Direction for DefaultLayout {
_ => unreachable!(),
},
Self::Grid => grid_neighbor(op_direction, idx, count),
Self::Scrolling => idx + 1,
}
}
}

View File

@@ -1,6 +1,7 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::str::FromStr;
@@ -108,6 +109,7 @@ pub enum SocketMessage {
AdjustWorkspacePadding(Sizing, i32),
ChangeLayout(DefaultLayout),
CycleLayout(CycleDirection),
ScrollingLayoutColumns(NonZeroUsize),
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
FlipLayout(Axis),
ToggleWorkspaceWindowContainerBehaviour,
@@ -200,6 +202,7 @@ pub enum SocketMessage {
StackbarFontFamily(Option<String>),
WorkAreaOffset(Rect),
MonitorWorkAreaOffset(usize, Rect),
WorkspaceWorkAreaOffset(usize, usize, Rect),
ToggleWindowBasedWorkAreaOffset,
ResizeDelta(i32),
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),

View File

@@ -41,6 +41,10 @@ impl Rect {
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
self.right == rhs.right && self.bottom == rhs.bottom
}
pub fn has_same_position_as(&self, rhs: &Self) -> bool {
self.left == rhs.left && self.top == rhs.top
}
}
impl Rect {

View File

@@ -8,7 +8,7 @@ pub mod ring;
pub mod container;
pub mod core;
pub mod focus_manager;
pub mod locked_deque;
pub mod lockable_sequence;
pub mod monitor;
pub mod monitor_reconciliator;
pub mod process_command;
@@ -200,8 +200,7 @@ lazy_static! {
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
);
@@ -255,6 +254,14 @@ pub static WINDOW_HANDLING_BEHAVIOUR: AtomicCell<WindowHandlingBehaviour> =
shadow_rs::shadow!(build);
/// A trait for types that can be marked as locked or unlocked.
pub trait Lockable {
/// Returns `true` if the item is locked.
fn locked(&self) -> bool;
/// Sets the locked state of the item.
fn set_locked(&mut self, locked: bool) -> &mut Self;
}
#[must_use]
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
@@ -299,7 +306,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
current
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum NotificationEvent {
@@ -316,7 +323,7 @@ pub enum VirtualDesktopNotification {
LeftAssociatedVirtualDesktop,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Notification {
pub event: NotificationEvent,

View File

@@ -0,0 +1,357 @@
use std::collections::VecDeque;
use crate::Lockable;
/// A sequence supporting insertion, removal, and swapping of elements while preserving the absolute
/// positions of locked items.
pub trait LockableSequence<T: Lockable> {
/// Inserts a value at `idx`, keeping locked elements at their absolute positions.
fn insert_respecting_locks(&mut self, idx: usize, value: T) -> usize;
/// Removes the element at `idx`, keeping locked elements at their absolute positions.
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T>;
/// Swaps the elements at indices `i` and `j`, keeping locked elements at their absolute positions.
fn swap_respecting_locks(&mut self, i: usize, j: usize);
}
impl<T: Lockable> LockableSequence<T> for VecDeque<T> {
/// Insert `value` at logical index `idx`, with trying to keep locked elements
/// (`is_locked()`) anchored at their original positions.
///
/// Returns the final index of the inserted element.
fn insert_respecting_locks(&mut self, mut idx: usize, value: T) -> usize {
// 1. Bounds check: if index is out of range, simply append.
if idx >= self.len() {
self.push_back(value);
return self.len() - 1; // last index
}
// 2. Normal VecDeque insertion
self.insert(idx, value);
// 3. Walk left-to-right once, swapping any misplaced locked element. After
// the VecDeque::insert all items after `idx` have moved right by one. For every locked
// element that is now to the right of an unlocked one, swap it back left exactly once.
for index in (idx + 1)..self.len() {
if self[index].locked() && !self[index - 1].locked() {
self.swap(index - 1, index);
// If the element we just inserted participated in the swap,
// update `idx` so we can return its final location.
if idx == index - 1 {
idx = index;
}
}
}
idx
}
/// Remove element at `idx`, with trying to keep locked elements
/// (`is_locked()`) anchored at their original positions.
///
/// Returns the removed element, or `None` if `idx` is out of bounds.
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T> {
// 1. Bounds check: if index is out of range, do nothing.
if idx >= self.len() {
return None;
}
// 2. Remove the element at the requested index.
// All elements after idx are now shifted left by 1.
let removed = self.remove(idx)?;
// 3. If less than 2 elements remain, nothing to shift.
if self.len() < 2 {
return Some(removed);
}
// 4. Iterate from the element just after the removed spot up to the second-to-last
// element, right-to-left. This loop "fixes" locked elements that were shifted left
// off their anchored positions: If a locked element now has an unlocked element
// to its right, swap them back to restore locked order.
for index in (idx..self.len() - 1).rev() {
// If current is locked and the next one is not locked, swap them.
if self[index].locked() && !self[index + 1].locked() {
self.swap(index, index + 1);
}
}
// 5. Return the removed value.
Some(removed)
}
/// Swaps the elements at indices `i` and `j`, along with their `locked` status, ensuring
/// the lock state remains associated with the position rather than the element itself.
fn swap_respecting_locks(&mut self, i: usize, j: usize) {
self.swap(i, j);
let locked_i = self[i].locked();
let locked_j = self[j].locked();
self[i].set_locked(locked_j);
self[j].set_locked(locked_i);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
struct TestItem {
val: i32,
locked: bool,
}
impl Lockable for TestItem {
fn locked(&self) -> bool {
self.locked
}
fn set_locked(&mut self, locked: bool) -> &mut Self {
self.locked = locked;
self
}
}
fn vals(v: &VecDeque<TestItem>) -> Vec<i32> {
v.iter().map(|x| x.val).collect()
}
fn test_deque(items: &[(i32, bool)]) -> VecDeque<TestItem> {
items
.iter()
.cloned()
.map(|(val, locked)| TestItem { val, locked })
.collect()
}
#[test]
fn test_insert_respecting_locks() {
// Test case 1: Basic insertion with locked index
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
// Insert at index 0, should shift elements while keeping index 2 locked
ring.insert_respecting_locks(
0,
TestItem {
val: 99,
locked: false,
},
);
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
assert_eq!(vals(&ring), vec![99, 0, 2, 1, 3, 4]);
}
// Test case 2: Insert at a locked index (should insert after locked)
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
// Try to insert at locked index 2, should insert at index 3 instead
let actual_index = ring.insert_respecting_locks(
2,
TestItem {
val: 99,
locked: false,
},
);
assert_eq!(actual_index, 3);
assert_eq!(vals(&ring), vec![0, 1, 2, 99, 3, 4]);
}
// Test case 3: Multiple locked indices
{
// Lock index 1 and 3
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
// Insert at index 0, should maintain locked indices
ring.insert_respecting_locks(
0,
TestItem {
val: 99,
locked: false,
},
);
// Elements '1' and '3' remain at indices 1 and 3
assert_eq!(vals(&ring), vec![99, 1, 0, 3, 2, 4]);
}
// Test case 4: Insert at end
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
let actual_index = ring.insert_respecting_locks(
5,
TestItem {
val: 99,
locked: false,
},
);
assert_eq!(actual_index, 5);
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
}
// Test case 5: Empty ring
{
let mut ring = test_deque(&[]);
// Insert into empty deque
let actual_index = ring.insert_respecting_locks(
0,
TestItem {
val: 99,
locked: false,
},
);
assert_eq!(actual_index, 0);
assert_eq!(vals(&ring), vec![99]);
}
// Test case 6: All indices locked
{
// Lock all indices
let mut ring = test_deque(&[(0, true), (1, true), (2, true), (3, true), (4, true)]);
// Try to insert at index 2, should insert at the end
let actual_index = ring.insert_respecting_locks(
2,
TestItem {
val: 99,
locked: false,
},
);
assert_eq!(actual_index, 5);
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
}
// Test case 7: Consecutive locked indices
{
// Lock index 2 and 3
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, true), (4, false)]);
// Insert at index 1, should maintain consecutive locked indices
ring.insert_respecting_locks(
1,
TestItem {
val: 99,
locked: false,
},
);
// Elements '2' and '3' remain at indices 2 and 3
assert_eq!(vals(&ring), vec![0, 99, 2, 3, 1, 4]);
}
}
#[test]
fn test_remove_respecting_locks() {
// Test case 1: Remove a non-locked index before a locked index
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
let removed = ring.remove_respecting_locks(0);
assert_eq!(removed.map(|x| x.val), Some(0));
// Elements '2' remain at index 2
assert_eq!(vals(&ring), vec![1, 3, 2, 4]);
}
// Test case 2: Remove a locked index
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
let removed = ring.remove_respecting_locks(2);
assert_eq!(removed.map(|x| x.val), Some(2));
// Elements should stay at the same places
assert_eq!(vals(&ring), vec![0, 1, 3, 4]);
}
// Test case 3: Remove an index after a locked index
{
// Lock index 1
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false), (4, false)]);
let removed = ring.remove_respecting_locks(3);
assert_eq!(removed.map(|x| x.val), Some(3));
// Elements should stay at the same places
assert_eq!(vals(&ring), vec![0, 1, 2, 4]);
}
// Test case 4: Multiple locked indices
{
// Lock index 1 and 3
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
let removed = ring.remove_respecting_locks(0);
assert_eq!(removed.map(|x| x.val), Some(0));
// Elements '1' and '3' remain at indices '1' and '3'
assert_eq!(vals(&ring), vec![2, 1, 4, 3]);
}
// Test case 5: Remove the last element
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
let removed = ring.remove_respecting_locks(4);
assert_eq!(removed.map(|x| x.val), Some(4));
// Index 2 should still be at the same place
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
}
// Test case 6: Invalid index
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
let removed = ring.remove_respecting_locks(10);
assert_eq!(removed, None);
// Deque unchanged
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4]);
}
// Test case 7: Remove enough elements to make a locked index invalid
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true)]);
ring.remove_respecting_locks(0);
// Index 2 should now be '1'
assert_eq!(vals(&ring), vec![1, 2]);
}
// Test case 8: Removing an element before multiple locked indices
{
// Lock index 2 and 4
let mut ring = test_deque(&[
(0, false),
(1, false),
(2, true),
(3, false),
(4, true),
(5, false),
]);
let removed = ring.remove_respecting_locks(1);
assert_eq!(removed.map(|x| x.val), Some(1));
// Both indices should still be at the same place
assert_eq!(vals(&ring), vec![0, 3, 2, 5, 4]);
}
}
#[test]
fn test_swap_respecting_locks_various_cases() {
// Swap unlocked and locked
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false)]);
ring.swap_respecting_locks(0, 1);
assert_eq!(vals(&ring), vec![1, 0, 2, 3]);
assert!(!ring[0].locked);
assert!(ring[1].locked);
ring.swap_respecting_locks(0, 1);
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
assert!(!ring[0].locked);
assert!(ring[1].locked);
// Both locked
let mut ring = test_deque(&[(0, true), (1, false), (2, true)]);
ring.swap_respecting_locks(0, 2);
assert_eq!(vals(&ring), vec![2, 1, 0]);
assert!(ring[0].locked);
assert!(!ring[1].locked);
assert!(ring[2].locked);
// Both unlocked
let mut ring = test_deque(&[(0, false), (1, true), (2, false)]);
ring.swap_respecting_locks(0, 2);
assert_eq!(vals(&ring), vec![2, 1, 0]);
assert!(!ring[0].locked);
assert!(ring[1].locked);
assert!(!ring[2].locked);
}
}

View File

@@ -1,316 +0,0 @@
use std::collections::BTreeSet;
use std::collections::VecDeque;
pub struct LockedDeque<'a, T> {
deque: &'a mut VecDeque<T>,
locked_indices: &'a mut BTreeSet<usize>,
}
impl<'a, T: PartialEq> LockedDeque<'a, T> {
pub fn new(deque: &'a mut VecDeque<T>, locked_indices: &'a mut BTreeSet<usize>) -> Self {
Self {
deque,
locked_indices,
}
}
pub fn insert(&mut self, index: usize, value: T) -> usize {
insert_respecting_locks(self.deque, self.locked_indices, index, value)
}
pub fn remove(&mut self, index: usize) -> Option<T> {
remove_respecting_locks(self.deque, self.locked_indices, index)
}
}
pub fn insert_respecting_locks<T>(
deque: &mut VecDeque<T>,
locked_idx: &mut BTreeSet<usize>,
idx: usize,
value: T,
) -> usize {
if idx == deque.len() {
deque.push_back(value);
return idx;
}
let mut new_deque = VecDeque::with_capacity(deque.len() + 1);
let mut temp_locked_deque = VecDeque::new();
let mut j = 0;
let mut corrected_idx = idx;
for (i, el) in deque.drain(..).enumerate() {
if i == idx {
corrected_idx = j;
}
if locked_idx.contains(&i) {
temp_locked_deque.push_back(el);
} else {
new_deque.push_back(el);
j += 1;
}
}
new_deque.insert(corrected_idx, value);
for (locked_el, locked_idx) in temp_locked_deque.into_iter().zip(locked_idx.iter()) {
new_deque.insert(*locked_idx, locked_el);
if *locked_idx <= corrected_idx {
corrected_idx += 1;
}
}
*deque = new_deque;
corrected_idx
}
pub fn remove_respecting_locks<T>(
deque: &mut VecDeque<T>,
locked_idx: &mut BTreeSet<usize>,
idx: usize,
) -> Option<T> {
if idx >= deque.len() {
return None;
}
let final_size = deque.len() - 1;
let mut new_deque = VecDeque::with_capacity(final_size);
let mut temp_locked_deque = VecDeque::new();
let mut removed = None;
let mut removed_locked_idx = None;
for (i, el) in deque.drain(..).enumerate() {
if i == idx {
removed = Some(el);
removed_locked_idx = locked_idx.contains(&i).then_some(i);
} else if locked_idx.contains(&i) {
temp_locked_deque.push_back(el);
} else {
new_deque.push_back(el);
}
}
if let Some(i) = removed_locked_idx {
let mut above = locked_idx.split_off(&i);
above.pop_first();
locked_idx.extend(above.into_iter().map(|i| i - 1));
}
while locked_idx.last().is_some_and(|i| *i >= final_size) {
locked_idx.pop_last();
}
let extra_invalid_idx = (new_deque.len()
..(new_deque.len() + temp_locked_deque.len() - locked_idx.len()))
.collect::<Vec<_>>();
for (locked_el, locked_idx) in temp_locked_deque
.into_iter()
.zip(locked_idx.iter().chain(extra_invalid_idx.iter()))
{
new_deque.insert(*locked_idx, locked_el);
}
*deque = new_deque;
removed
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
use std::collections::VecDeque;
#[test]
fn test_insert_respecting_locks() {
// Test case 1: Basic insertion with locked index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
// Insert at index 0, should shift elements while keeping index 2 locked
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
assert_eq!(deque, VecDeque::from(vec![99, 0, 2, 1, 3, 4]));
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
}
// Test case 2: Insert at a locked index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
// Try to insert at locked index 2, should insert at index 3 instead
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
assert_eq!(actual_index, 3);
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 99, 3, 4]));
}
// Test case 3: Multiple locked indices
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(1); // Lock index 1
locked.insert(3); // Lock index 3
// Insert at index 0, should maintain locked indices
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
assert_eq!(deque, VecDeque::from(vec![99, 1, 0, 3, 2, 4]));
// Elements '1' and '3' remain at indices 1 and 3
}
// Test case 4: Insert at end
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
// Insert at end of deque
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 5, 99);
assert_eq!(actual_index, 5);
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
}
// Test case 5: Empty deque
{
let mut deque = VecDeque::new();
let mut locked = BTreeSet::new();
// Insert into empty deque
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 0, 99);
assert_eq!(actual_index, 0);
assert_eq!(deque, VecDeque::from(vec![99]));
}
// Test case 6: All indices locked
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
for i in 0..5 {
locked.insert(i); // Lock all indices
}
// Try to insert at index 2, should insert at the end
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
assert_eq!(actual_index, 5);
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
}
// Test case 7: Consecutive locked indices
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
locked.insert(3); // Lock index 3
// Insert at index 1, should maintain consecutive locked indices
insert_respecting_locks(&mut deque, &mut locked, 1, 99);
assert_eq!(deque, VecDeque::from(vec![0, 99, 2, 3, 1, 4]));
// Elements '2' and '3' remain at indices 2 and 3
}
}
#[test]
fn test_remove_respecting_locks() {
// Test case 1: Remove a non-locked index before a locked index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
assert_eq!(removed, Some(0));
assert_eq!(deque, VecDeque::from(vec![1, 3, 2, 4]));
assert!(locked.contains(&2)); // Index 2 should still be locked
}
// Test case 2: Remove a locked index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
let removed = remove_respecting_locks(&mut deque, &mut locked, 2);
assert_eq!(removed, Some(2));
assert_eq!(deque, VecDeque::from(vec![0, 1, 3, 4]));
assert!(!locked.contains(&2)); // Index 2 should be unlocked
}
// Test case 3: Remove an index after a locked index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(1); // Lock index 1
let removed = remove_respecting_locks(&mut deque, &mut locked, 3);
assert_eq!(removed, Some(3));
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 4]));
assert!(locked.contains(&1)); // Index 1 should still be locked
}
// Test case 4: Multiple locked indices
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(1); // Lock index 1
locked.insert(3); // Lock index 3
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
assert_eq!(removed, Some(0));
assert_eq!(deque, VecDeque::from(vec![2, 1, 4, 3]));
assert!(locked.contains(&1) && locked.contains(&3)); // Both indices should still be locked
}
// Test case 5: Remove the last element
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
let removed = remove_respecting_locks(&mut deque, &mut locked, 4);
assert_eq!(removed, Some(4));
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3]));
assert!(locked.contains(&2)); // Index 2 should still be locked
}
// Test case 6: Invalid index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
let removed = remove_respecting_locks(&mut deque, &mut locked, 10);
assert_eq!(removed, None);
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4])); // Deque unchanged
assert!(locked.contains(&2)); // Lock unchanged
}
// Test case 7: Remove enough elements to make a locked index invalid
{
let mut deque = VecDeque::from(vec![0, 1, 2]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
remove_respecting_locks(&mut deque, &mut locked, 0);
assert_eq!(deque, VecDeque::from(vec![1, 2]));
assert!(!locked.contains(&2)); // Index 2 should now be invalid
}
// Test case 8: Removing an element before multiple locked indices
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4, 5]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
locked.insert(4); // Lock index 4
let removed = remove_respecting_locks(&mut deque, &mut locked, 1);
assert_eq!(removed, Some(1));
assert_eq!(deque, VecDeque::from(vec![0, 3, 2, 5, 4]));
assert!(locked.contains(&2) && locked.contains(&4)); // Both indices should still be locked
}
}
}

View File

@@ -500,7 +500,7 @@ impl Monitor {
if workspaces.get(idx).is_none() {
workspaces.resize(idx + 1, Workspace::default());
}
self.set_last_focused_workspace(Some(self.workspaces.focused_idx()));
self.workspaces.focus(idx);
}

View File

@@ -801,7 +801,7 @@ mod tests {
let wm = match WindowManager::new(receiver, Some(socket_path.clone())) {
Ok(manager) => manager,
Err(e) => {
panic!("Failed to create WindowManager: {}", e);
panic!("Failed to create WindowManager: {e}");
}
};
@@ -829,7 +829,7 @@ mod tests {
Ok(notification) => {
assert_eq!(notification, MonitorNotification::ResolutionScalingChanged);
}
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
}
}
@@ -849,7 +849,7 @@ mod tests {
for _ in 0..20 {
let notification = match receiver.try_recv() {
Ok(notification) => notification,
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
};
assert_eq!(
notification,
@@ -960,7 +960,7 @@ mod tests {
Ok(notification) => {
assert_eq!(notification, MonitorNotification::DisplayConnectionChange);
}
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
}
}

View File

@@ -49,6 +49,8 @@ use crate::core::StateQuery;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowKind;
use crate::current_virtual_desktop;
use crate::default_layout::LayoutOptions;
use crate::default_layout::ScrollingLayoutOptions;
use crate::monitor::MonitorInformation;
use crate::notify_subscribers;
use crate::stackbar_manager;
@@ -207,25 +209,6 @@ impl WindowManager {
// We don't have From implemented for &mut WindowManager
let initial_state = State::from(self.as_ref());
match message {
SocketMessage::CycleFocusEmptyWorkspace(_)
| SocketMessage::CycleFocusWorkspace(_)
| SocketMessage::FocusWorkspaceNumber(_) => {
if let Some(monitor) = self.focused_monitor_mut() {
let idx = monitor.focused_workspace_idx();
monitor.set_last_focused_workspace(Option::from(idx));
}
}
SocketMessage::FocusMonitorWorkspaceNumber(target_monitor_idx, _) => {
let idx = self.focused_workspace_idx_for_monitor_idx(target_monitor_idx)?;
if let Some(monitor) = self.monitors_mut().get_mut(target_monitor_idx) {
monitor.set_last_focused_workspace(Option::from(idx));
}
}
_ => {}
};
let mut force_update_borders = false;
match message {
SocketMessage::Promote => self.promote_container_to_front()?,
@@ -347,7 +330,7 @@ impl WindowManager {
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
SocketMessage::StackAll => self.stack_all()?,
SocketMessage::UnstackAll => self.unstack_all()?,
SocketMessage::UnstackAll => self.unstack_all(true)?,
SocketMessage::CycleStack(direction) => {
self.cycle_container_window_in_direction(direction)?;
}
@@ -391,7 +374,9 @@ impl WindowManager {
.get_mut(workspace_idx)
.ok_or_eyre("no workspace at the given index")?;
workspace.locked_containers.insert(container_idx);
if let Some(container) = workspace.containers_mut().get_mut(container_idx) {
container.set_locked(true);
}
}
SocketMessage::UnlockMonitorWorkspaceContainer(
monitor_idx,
@@ -408,7 +393,9 @@ impl WindowManager {
.get_mut(workspace_idx)
.ok_or_eyre("no workspace at the given index")?;
workspace.locked_containers.remove(&container_idx);
if let Some(container) = workspace.containers_mut().get_mut(container_idx) {
container.set_locked(false);
}
}
SocketMessage::ToggleLock => self.toggle_lock()?,
SocketMessage::ToggleFloat => self.toggle_float(false)?,
@@ -933,6 +920,27 @@ impl WindowManager {
self.retile_all(true)?
}
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
SocketMessage::ScrollingLayoutColumns(count) => {
let focused_workspace = self.focused_workspace_mut()?;
let options = match focused_workspace.layout_options() {
Some(mut opts) => {
if let Some(scrolling) = &mut opts.scrolling {
scrolling.columns = count.into();
}
opts
}
None => LayoutOptions {
scrolling: Some(ScrollingLayoutOptions {
columns: count.into(),
}),
},
};
focused_workspace.set_layout_options(Some(options));
self.update_focused_workspace(false, false)?;
}
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
SocketMessage::ChangeLayoutCustom(ref path) => {
@@ -1751,7 +1759,7 @@ Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue
{
for config_file_path in &mut *display_bar_configurations {
let script = r#"Start-Process "komorebi-bar" '"--config" "CONFIGFILE"' -WindowStyle hidden"#
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
match powershell_script::run(&script) {
Ok(_) => {
@@ -1878,6 +1886,14 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
self.retile_all(false)?;
}
}
SocketMessage::WorkspaceWorkAreaOffset(monitor_idx, workspace_idx, rect) => {
if let Some(monitor) = self.monitors_mut().get_mut(monitor_idx) {
if let Some(workspace) = monitor.workspaces_mut().get_mut(workspace_idx) {
workspace.set_work_area_offset(Option::from(rect));
self.retile_all(false)?
}
}
}
SocketMessage::ToggleWindowBasedWorkAreaOffset => {
let workspace = self.focused_workspace_mut()?;
workspace.set_apply_window_based_work_area_offset(

View File

@@ -25,6 +25,8 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::workspace::WorkspaceLayer;
use crate::DefaultLayout;
use crate::Layout;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
@@ -301,7 +303,11 @@ impl WindowManager {
// don't want to trigger the full workspace updates when there are no managed
// containers - this makes floating windows on empty workspaces go into very
// annoying focus change loops which prevents users from interacting with them
if !self.focused_workspace()?.containers().is_empty() {
if !matches!(
self.focused_workspace()?.layout(),
Layout::Default(DefaultLayout::Scrolling)
) && !self.focused_workspace()?.containers().is_empty()
{
self.update_focused_workspace(self.mouse_follows_focus, false)?;
}
@@ -328,6 +334,14 @@ impl WindowManager {
}
workspace.set_layer(WorkspaceLayer::Tiling);
if matches!(
self.focused_workspace()?.layout(),
Layout::Default(DefaultLayout::Scrolling)
) && !self.focused_workspace()?.containers().is_empty()
{
self.update_focused_workspace(self.mouse_follows_focus, false)?;
}
}
Some(idx) => {
if let Some(_window) = workspace.floating_windows().get(idx) {

View File

@@ -43,10 +43,6 @@ impl<T> Ring<T> {
pub fn focused_mut(&mut self) -> Option<&mut T> {
self.elements.get_mut(self.focused)
}
pub fn swap(&mut self, i: usize, j: usize) {
self.elements.swap(i, j);
}
}
macro_rules! impl_ring_elements {

View File

@@ -28,7 +28,7 @@ pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947);
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Title);
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::Never);
pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);

View File

@@ -58,15 +58,19 @@ use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
@@ -312,6 +316,16 @@ impl Stackbar {
) -> LRESULT {
unsafe {
match msg {
WM_SETCURSOR => match LoadCursorW(None, IDC_ARROW) {
Ok(cursor) => {
SetCursor(Some(cursor));
LRESULT(0)
}
Err(error) => {
tracing::error!("{error}");
LRESULT(1)
}
},
WM_LBUTTONDOWN => {
let stackbars_containers = STACKBARS_CONTAINERS.lock();
if let Some(container) = stackbars_containers.get(&(hwnd.0 as isize)) {

View File

@@ -35,6 +35,7 @@ use crate::core::StackbarMode;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowManagementBehaviour;
use crate::current_virtual_desktop;
use crate::default_layout::LayoutOptions;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator;
@@ -191,6 +192,9 @@ pub struct WorkspaceConfig {
/// Layout (default: BSP)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout: Option<DefaultLayout>,
/// Layout-specific options (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_options: Option<LayoutOptions>,
/// END OF LIFE FEATURE: Custom Layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<ResolvedPathBuf>")]
@@ -214,6 +218,9 @@ pub struct WorkspaceConfig {
/// Permanent workspace application rules
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_rules: Option<Vec<MatchingRule>>,
/// Workspace specific work area offset (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub work_area_offset: Option<Rect>,
/// Apply this monitor's window-based work area offset (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
pub apply_window_based_work_area_offset: Option<bool>,
@@ -226,6 +233,9 @@ pub struct WorkspaceConfig {
/// Enable or disable float override, which makes it so every new window opens in floating mode (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
pub float_override: Option<bool>,
/// Enable or disable tiling for the workspace (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
pub tile: Option<bool>,
/// Specify an axis on which to flip the selected layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_flip: Option<Axis>,
@@ -274,6 +284,8 @@ impl From<&Workspace> for WorkspaceConfig {
}
});
let tile = if *value.tile() { None } else { Some(false) };
Self {
name: value
.name()
@@ -286,6 +298,7 @@ impl From<&Workspace> for WorkspaceConfig {
Layout::Custom(_) => None,
})
.flatten(),
layout_options: value.layout_options(),
custom_layout: value
.workspace_config()
.as_ref()
@@ -305,10 +318,12 @@ impl From<&Workspace> for WorkspaceConfig {
.workspace_config()
.as_ref()
.and_then(|c| c.workspace_rules.clone()),
work_area_offset: value.work_area_offset(),
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
window_container_behaviour: *value.window_container_behaviour(),
window_container_behaviour_rules: Option::from(window_container_behaviour_rules),
float_override: *value.float_override(),
tile,
layout_flip: value.layout_flip(),
floating_layer_behaviour: value.floating_layer_behaviour(),
wallpaper: None,
@@ -397,7 +412,7 @@ pub enum AppSpecificConfigurationPath {
#[serde_with::serde_as]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// The `komorebi.json` static configuration file reference for `v0.1.37`
/// The `komorebi.json` static configuration file reference for `v0.1.38`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1331,7 +1346,7 @@ impl StaticConfig {
}
pub fn postload(path: &PathBuf, wm: &Arc<Mutex<WindowManager>>) -> Result<()> {
let value = Self::read(path)?;
let mut value = Self::read(path)?;
let mut wm = wm.lock();
let configs_with_preference: Vec<_> =
@@ -1342,6 +1357,8 @@ impl StaticConfig {
workspace_matching_rules.clear();
drop(workspace_matching_rules);
let monitor_count = wm.monitors().len();
let offset = wm.work_area_offset;
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let preferred_config_idx = {
@@ -1371,8 +1388,8 @@ impl StaticConfig {
});
if let Some(monitor_config) = value
.monitors
.as_ref()
.and_then(|ms| idx.and_then(|i| ms.get(i)))
.as_mut()
.and_then(|ms| idx.and_then(|i| ms.get_mut(i)))
{
if let Some(used_config_idx) = idx {
configs_used.push(used_config_idx);
@@ -1395,7 +1412,14 @@ impl StaticConfig {
monitor.update_workspaces_globals(offset);
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
if let Some(workspace_config) = monitor_config.workspaces.get_mut(j) {
if monitor_count > 1
&& matches!(workspace_config.layout, Some(DefaultLayout::Scrolling))
{
tracing::warn!("scrolling layout is only supported for a single monitor; falling back to columns layout");
workspace_config.layout = Some(DefaultLayout::Columns);
}
ws.load_static_config(workspace_config)?;
}
}
@@ -1918,7 +1942,7 @@ mod tests {
let docs = vec![
"0.1.20", "0.1.21", "0.1.22", "0.1.23", "0.1.24", "0.1.25", "0.1.26", "0.1.27",
"0.1.28", "0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33", "0.1.34", "0.1.35",
"0.1.36",
"0.1.36", "0.1.37",
];
let mut versions = vec![];

View File

@@ -126,7 +126,7 @@ pub struct WindowManager {
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct State {
pub monitors: Ring<Monitor>,
@@ -329,6 +329,7 @@ impl From<&WindowManager> for State {
maximized_window_restore_idx: workspace.maximized_window_restore_idx,
floating_windows: workspace.floating_windows.clone(),
layout: workspace.layout.clone(),
layout_options: workspace.layout_options,
layout_rules: workspace.layout_rules.clone(),
layout_flip: workspace.layout_flip,
workspace_padding: workspace.workspace_padding,
@@ -336,6 +337,7 @@ impl From<&WindowManager> for State {
latest_layout: workspace.latest_layout.clone(),
resize_dimensions: workspace.resize_dimensions.clone(),
tile: workspace.tile,
work_area_offset: workspace.work_area_offset,
apply_window_based_work_area_offset: workspace
.apply_window_based_work_area_offset,
window_container_behaviour: workspace.window_container_behaviour,
@@ -346,7 +348,6 @@ impl From<&WindowManager> for State {
layer: workspace.layer,
floating_layer_behaviour: workspace.floating_layer_behaviour,
globals: workspace.globals,
locked_containers: workspace.locked_containers.clone(),
wallpaper: workspace.wallpaper.clone(),
workspace_config: None,
})
@@ -821,7 +822,7 @@ impl WindowManager {
target_workspace_idx: usize,
floating: bool,
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
) -> () {
) {
tracing::trace!(
"{} should be on monitor {}, workspace {}",
window_title,
@@ -1579,6 +1580,9 @@ impl WindowManager {
workspace.container_padding(),
workspace.layout_flip(),
&[],
workspace.focused_container_idx(),
workspace.layout_options(),
workspace.latest_layout(),
);
let mut direction = direction;
@@ -2956,6 +2960,8 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn stack_all(&mut self) -> Result<()> {
self.unstack_all(false)?;
self.handle_unmanaged_window_behaviour()?;
tracing::info!("stacking all windows on workspace");
@@ -2982,7 +2988,7 @@ impl WindowManager {
}
#[tracing::instrument(skip(self))]
pub fn unstack_all(&mut self) -> Result<()> {
pub fn unstack_all(&mut self, update_workspace: bool) -> Result<()> {
self.handle_unmanaged_window_behaviour()?;
tracing::info!("unstacking all windows in container");
@@ -3012,7 +3018,11 @@ impl WindowManager {
workspace.focus_container_by_window(hwnd)?;
}
self.update_focused_workspace(self.mouse_follows_focus, true)
if update_workspace {
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
Ok(())
}
#[tracing::instrument(skip(self))]
@@ -3182,14 +3192,10 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn toggle_lock(&mut self) -> Result<()> {
let workspace = self.focused_workspace_mut()?;
let index = workspace.focused_container_idx();
if workspace.locked_containers().contains(&index) {
workspace.locked_containers_mut().remove(&index);
} else {
workspace.locked_containers_mut().insert(index);
if let Some(container) = workspace.focused_container_mut() {
// Toggle the locked flag
container.set_locked(!container.locked());
}
Ok(())
}
@@ -3352,8 +3358,16 @@ impl WindowManager {
pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> Result<()> {
tracing::info!("changing layout");
let monitor_count = self.monitors().len();
let workspace = self.focused_workspace_mut()?;
if monitor_count > 1 && matches!(layout, DefaultLayout::Scrolling) {
tracing::warn!(
"scrolling layout is only supported for a single monitor; not changing layout"
);
return Ok(());
}
match workspace.layout() {
Layout::Default(_) => {}
Layout::Custom(layout) => {
@@ -4759,6 +4773,44 @@ mod tests {
}
}
#[test]
fn test_remove_nonexistent_window_from_container() {
let (mut wm, _context) = setup_window_manager();
{
// Create a first monitor
let mut m = monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor1".to_string(),
"TestDevice1".to_string(),
"TestDeviceID1".to_string(),
Some("TestMonitorID1".to_string()),
);
// Create a container
let container = Container::default();
// Should have 3 windows in the container
assert_eq!(container.windows().len(), 0);
// Add the container to a workspace
let workspace = m.focused_workspace_mut().unwrap();
workspace.add_container_to_back(container);
// Add monitor to the window manager
wm.monitors_mut().push_back(m);
}
// Should receive an error when trying to remove a window from an empty container
let result = wm.remove_window_from_container();
assert!(
result.is_err(),
"Expected an error when trying to remove a window from an empty container"
);
}
#[test]
fn cycle_container_window_in_direction() {
let (mut wm, _context) = setup_window_manager();
@@ -4828,6 +4880,44 @@ mod tests {
}
}
#[test]
fn test_cycle_nonexistent_windows() {
let (mut wm, _context) = setup_window_manager();
{
// Create a first monitor
let mut m = monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor1".to_string(),
"TestDevice1".to_string(),
"TestDeviceID1".to_string(),
Some("TestMonitorID1".to_string()),
);
// Create a container
let container = Container::default();
// Should have 3 windows in the container
assert_eq!(container.windows().len(), 0);
// Add the container to a workspace
let workspace = m.focused_workspace_mut().unwrap();
workspace.add_container_to_back(container);
// Add monitor to the window manager
wm.monitors_mut().push_back(m);
}
// Should return an error when trying to cycle through windows in an empty container
let result = wm.cycle_container_window_in_direction(CycleDirection::Next);
assert!(
result.is_err(),
"Expected an error when cycling through windows in an empty container"
);
}
#[test]
fn test_cycle_container_window_index_in_direction() {
let (mut wm, _context) = setup_window_manager();
@@ -5357,7 +5447,8 @@ mod tests {
{
// Ensure container 2 is not locked
let workspace = wm.focused_workspace_mut().unwrap();
assert!(!workspace.locked_containers().contains(&2));
assert_eq!(workspace.focused_container_idx(), 2);
assert!(!workspace.focused_container().unwrap().locked());
}
// Toggle lock on focused container
@@ -5366,7 +5457,7 @@ mod tests {
{
// Ensure container 2 is locked
let workspace = wm.focused_workspace_mut().unwrap();
assert!(workspace.locked_containers().contains(&2));
assert!(workspace.focused_container().unwrap().locked());
}
// Toggle lock on focused container
@@ -5375,7 +5466,7 @@ mod tests {
{
// Ensure container 2 is not locked
let workspace = wm.focused_workspace_mut().unwrap();
assert!(!workspace.locked_containers().contains(&2));
assert!(!workspace.focused_container().unwrap().locked());
}
}
@@ -5457,6 +5548,40 @@ mod tests {
}
}
#[test]
fn test_float_nonexistent_window() {
let (mut wm, _context) = setup_window_manager();
{
let mut m = monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
// Add another workspace
let new_workspace_index = m.new_workspace_idx();
m.focus_workspace(new_workspace_index).unwrap();
// Should have 2 workspaces
assert_eq!(m.workspaces().len(), 2);
// Add monitor to window manager
wm.monitors_mut().push_back(m);
}
// Should return an error when trying to float a non-existent window
let result = wm.float_window();
assert!(
result.is_err(),
"Expected an error when trying to float a non-existent window"
);
}
#[test]
fn test_maximize_and_unmaximize_window() {
let (mut wm, _context) = setup_window_manager();
@@ -5603,6 +5728,41 @@ mod tests {
}
}
#[test]
fn test_toggle_maximize_nonexistent_window() {
let (mut wm, _context) = setup_window_manager();
{
// Create a monitor
let mut m = monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
// Create a container
let container = Container::default();
// Add the container to the workspace
let workspace = m.focused_workspace_mut().unwrap();
workspace.add_container_to_back(container);
// Add monitor to the window manager
wm.monitors_mut().push_back(m);
}
// Should return an error when trying to toggle maximize on a non-existent window
let result = wm.toggle_maximize();
assert!(
result.is_err(),
"Expected an error when trying to toggle maximize on a non-existent window"
);
}
#[test]
fn test_monocle_on_and_monocle_off() {
let (mut wm, _context) = setup_window_manager();
@@ -5674,6 +5834,41 @@ mod tests {
}
}
#[test]
fn test_monocle_on_and_off_nonexistent_container() {
let (mut wm, _context) = setup_window_manager();
{
// Create a monitor
let m = monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
// Add monitor to the window manager
wm.monitors_mut().push_back(m);
}
// Should return an error when trying to move a non-existent container to monocle
let result = wm.monocle_on();
assert!(
result.is_err(),
"Expected an error when trying to move a non-existent container to monocle"
);
// Should return an error when trying to restore a non-existent container from monocle
let result = wm.monocle_off();
assert!(
result.is_err(),
"Expected an error when trying to restore a non-existent container from monocle"
);
}
#[test]
fn test_toggle_monocle() {
let (mut wm, _context) = setup_window_manager();
@@ -5745,6 +5940,34 @@ mod tests {
}
}
#[test]
fn test_toggle_monocle_nonexistent_container() {
let (mut wm, _context) = setup_window_manager();
{
// Create a monitor
let m = monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
// Add monitor to the window manager
wm.monitors_mut().push_back(m);
}
// Should return an error when trying to toggle monocle on a non-existent container
let result = wm.toggle_monocle();
assert!(
result.is_err(),
"Expected an error when trying to toggle monocle on a non-existent container"
);
}
#[test]
fn test_ensure_named_workspace_for_monitor() {
let (mut wm, _context) = setup_window_manager();

View File

@@ -1180,6 +1180,7 @@ impl WindowsApi {
#[allow(dead_code)]
pub fn enable_focus_follows_mouse() -> Result<()> {
#[allow(clippy::manual_dangling_ptr)]
Self::system_parameters_info_w(
SPI_SETACTIVEWINDOWTRACKING,
0,

View File

@@ -1,4 +1,3 @@
use std::collections::BTreeSet;
use std::collections::VecDeque;
use std::ffi::OsStr;
use std::fmt::Display;
@@ -16,7 +15,8 @@ use crate::core::DefaultLayout;
use crate::core::Layout;
use crate::core::OperationDirection;
use crate::core::Rect;
use crate::locked_deque::LockedDeque;
use crate::default_layout::LayoutOptions;
use crate::lockable_sequence::LockableSequence;
use crate::ring::Ring;
use crate::should_act;
use crate::stackbar_manager;
@@ -70,6 +70,8 @@ pub struct Workspace {
pub floating_windows: Ring<Window>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub layout: Layout,
#[getset(get_copy = "pub", set = "pub")]
pub layout_options: Option<LayoutOptions>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub layout_rules: Vec<(usize, Layout)>,
#[getset(get_copy = "pub", set = "pub")]
@@ -85,6 +87,8 @@ pub struct Workspace {
#[getset(get = "pub", set = "pub")]
pub tile: bool,
#[getset(get_copy = "pub", set = "pub")]
pub work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
pub apply_window_based_work_area_offset: bool,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub window_container_behaviour: Option<WindowContainerBehaviour>,
@@ -100,8 +104,6 @@ pub struct Workspace {
#[getset(get_copy = "pub", get_mut = "pub", set = "pub")]
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub locked_containers: BTreeSet<usize>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub wallpaper: Option<Wallpaper>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get = "pub", set = "pub")]
@@ -139,6 +141,7 @@ impl Default for Workspace {
monocle_container_restore_idx: None,
floating_windows: Ring::default(),
layout: Layout::Default(DefaultLayout::BSP),
layout_options: None,
layout_rules: vec![],
layout_flip: None,
workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)),
@@ -146,6 +149,7 @@ impl Default for Workspace {
latest_layout: vec![],
resize_dimensions: vec![],
tile: true,
work_area_offset: None,
apply_window_based_work_area_offset: true,
window_container_behaviour: None,
window_container_behaviour_rules: None,
@@ -154,7 +158,6 @@ impl Default for Workspace {
floating_layer_behaviour: Default::default(),
globals: Default::default(),
workspace_config: None,
locked_containers: Default::default(),
wallpaper: None,
}
}
@@ -205,18 +208,16 @@ impl Workspace {
if let Some(layout) = &config.layout {
self.layout = Layout::Default(*layout);
self.tile = true;
}
if let Some(pathbuf) = &config.custom_layout {
let layout = CustomLayout::from_path(pathbuf)?;
self.layout = Layout::Custom(layout);
self.tile = true;
}
if config.custom_layout.is_none() && config.layout.is_none() {
self.tile = false;
}
self.tile =
!(config.custom_layout.is_none() && config.layout.is_none() && config.tile.is_none()
|| config.tile.is_some_and(|tile| !tile));
let mut all_layout_rules = vec![];
if let Some(layout_rules) = &config.layout_rules {
@@ -241,6 +242,8 @@ impl Workspace {
self.set_layout_rules(all_layout_rules);
}
self.set_work_area_offset(config.work_area_offset);
self.set_apply_window_based_work_area_offset(
config.apply_window_based_work_area_offset.unwrap_or(true),
);
@@ -267,6 +270,7 @@ impl Workspace {
self.set_layout_flip(config.layout_flip);
self.set_floating_layer_behaviour(config.floating_layer_behaviour);
self.set_wallpaper(config.wallpaper.clone());
self.set_layout_options(config.layout_options);
self.set_workspace_config(Some(config.clone()));
@@ -495,7 +499,7 @@ impl Workspace {
let border_width = self.globals().border_width;
let border_offset = self.globals().border_offset;
let work_area = self.globals().work_area;
let work_area_offset = self.globals().work_area_offset;
let work_area_offset = self.work_area_offset().or(self.globals().work_area_offset);
let window_based_work_area_offset = self.globals().window_based_work_area_offset;
let window_based_work_area_offset_limit =
self.globals().window_based_work_area_offset_limit;
@@ -583,6 +587,9 @@ impl Workspace {
Some(container_padding),
self.layout_flip(),
self.resize_dimensions(),
self.focused_container_idx(),
self.layout_options(),
self.latest_layout(),
);
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
@@ -890,10 +897,9 @@ impl Workspace {
// this fn respects locked container indexes - we should use it for pretty much everything
// except monocle and maximize toggles
pub fn insert_container_at_idx(&mut self, idx: usize, container: Container) -> usize {
let mut locked_containers = self.locked_containers().clone();
let mut ld = LockedDeque::new(self.containers_mut(), &mut locked_containers);
let insertion_idx = ld.insert(idx, container);
self.locked_containers = locked_containers;
let insertion_idx = self
.containers_mut()
.insert_respecting_locks(idx, container);
if insertion_idx > self.resize_dimensions().len() {
self.resize_dimensions_mut().push(None);
@@ -909,10 +915,7 @@ impl Workspace {
// this fn respects locked container indexes - we should use it for pretty much everything
// except monocle and maximize toggles
pub fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
let mut locked_containers = self.locked_containers().clone();
let mut ld = LockedDeque::new(self.containers_mut(), &mut locked_containers);
let container = ld.remove(idx);
self.locked_containers = locked_containers;
let container = self.containers_mut().remove_respecting_locks(idx);
if idx < self.resize_dimensions().len() {
self.resize_dimensions_mut().remove(idx);
@@ -1194,6 +1197,9 @@ impl Workspace {
Layout::Default(DefaultLayout::UltrawideVerticalStack) => {
self.enforce_resize_for_ultrawide();
}
Layout::Default(DefaultLayout::Scrolling) => {
self.enforce_resize_for_scrolling();
}
_ => self.enforce_no_resize(),
}
}
@@ -1421,6 +1427,28 @@ impl Workspace {
}
}
fn enforce_resize_for_scrolling(&mut self) {
let resize_dimensions = self.resize_dimensions_mut();
match resize_dimensions.len() {
0 | 1 => self.enforce_no_resize(),
_ => {
let len = resize_dimensions.len();
for (i, rect) in resize_dimensions.iter_mut().enumerate() {
if let Some(rect) = rect {
rect.top = 0;
rect.bottom = 0;
if i == 0 {
rect.left = 0;
} else if i == len - 1 {
rect.right = 0;
}
}
}
}
}
}
fn enforce_no_resize(&mut self) {
for rect in self.resize_dimensions_mut().iter_mut().flatten() {
rect.left = 0;
@@ -1601,7 +1629,7 @@ impl Workspace {
}
pub fn swap_containers(&mut self, i: usize, j: usize) {
self.containers.swap(i, j);
self.containers.elements_mut().swap_respecting_locks(i, j);
self.focus_container(j);
}
@@ -1700,7 +1728,6 @@ mod tests {
use super::*;
use crate::container::Container;
use crate::Window;
use std::collections::BTreeSet;
use std::collections::HashMap;
#[test]
@@ -1708,20 +1735,18 @@ mod tests {
let mut ws = Workspace::default();
let mut state = HashMap::new();
let mut locked = BTreeSet::new();
// add 3 containers
// add 4 containers
for i in 0..4 {
let container = Container::default();
let mut container = Container::default();
if i == 3 {
container.set_locked(true); // set index 3 locked
}
state.insert(i, container.id().to_string());
ws.add_container_to_back(container);
}
assert_eq!(ws.containers().len(), 4);
// set index 3 locked
locked.insert(3);
ws.locked_containers = locked;
// focus container at index 2
ws.focus_container(2);
@@ -1753,20 +1778,17 @@ mod tests {
fn test_locked_containers_remove_window() {
let mut ws = Workspace::default();
let mut locked = BTreeSet::new();
// add 4 containers
for i in 0..4 {
let mut container = Container::default();
container.windows_mut().push_back(Window::from(i));
if i == 1 {
container.set_locked(true);
}
ws.add_container_to_back(container);
}
assert_eq!(ws.containers().len(), 4);
// set index 1 locked
locked.insert(1);
ws.locked_containers = locked;
ws.remove_window(0).unwrap();
assert_eq!(ws.containers()[0].focused_window().unwrap().hwnd, 2);
// index 1 should still be the same
@@ -1778,20 +1800,17 @@ mod tests {
fn test_locked_containers_toggle_float() {
let mut ws = Workspace::default();
let mut locked = BTreeSet::new();
// add 4 containers
for i in 0..4 {
let mut container = Container::default();
container.windows_mut().push_back(Window::from(i));
if i == 1 {
container.set_locked(true);
}
ws.add_container_to_back(container);
}
assert_eq!(ws.containers().len(), 4);
// set index 1 locked
locked.insert(1);
ws.locked_containers = locked;
// set index 0 focused
ws.focus_container(0);
@@ -1823,20 +1842,17 @@ mod tests {
fn test_locked_containers_stack() {
let mut ws = Workspace::default();
let mut locked = BTreeSet::new();
// add 6 containers
for i in 0..6 {
let mut container = Container::default();
container.windows_mut().push_back(Window::from(i));
if i == 4 {
container.set_locked(true);
}
ws.add_container_to_back(container);
}
assert_eq!(ws.containers().len(), 6);
// set index 4 locked
locked.insert(4);
ws.locked_containers = locked;
// set index 3 focused
ws.focus_container(3);

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.37"
version = "0.1.38"
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"

View File

@@ -184,6 +184,10 @@ MonitorWorkAreaOffset(monitor, left, top, right, bottom) {
RunWait("komorebic.exe monitor-work-area-offset " monitor " " left " " top " " right " " bottom, , "Hide")
}
WorkspaceWorkAreaOffset(monitor, workspace, left, top, right, bottom) {
RunWait("komorebic.exe workspace-work-area-offset " monitor " "workspace" " left " " top " " right " " bottom, , "Hide")
}
AdjustContainerPadding(sizing, adjustment) {
RunWait("komorebic.exe adjust-container-padding " sizing " " adjustment, , "Hide")
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic"
version = "0.1.37"
version = "0.1.38"
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"

View File

@@ -6,9 +6,11 @@ use komorebi_client::replace_env_in_path;
use komorebi_client::PathExt;
use std::fs::File;
use std::fs::OpenOptions;
use std::io;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::AtomicBool;
@@ -91,8 +93,7 @@ lazy_static! {
assert!(
whkd_config_home.is_dir(),
"$Env:WHKD_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
"$Env:WHKD_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
);
whkd_config_home
@@ -428,6 +429,22 @@ struct MonitorWorkAreaOffset {
bottom: i32,
}
#[derive(Parser)]
struct WorkspaceWorkAreaOffset {
/// Monitor index (zero-indexed)
monitor: usize,
/// Workspace index (zero-indexed)
workspace: usize,
/// Size of the left work area offset (set right to left * 2 to maintain right padding)
left: i32,
/// Size of the top work area offset (set bottom to the same value to maintain bottom padding)
top: i32,
/// Size of the right work area offset
right: i32,
/// Size of the bottom work area offset
bottom: i32,
}
#[derive(Parser)]
struct MonitorIndexPreference {
/// Preferred monitor index (zero-indexed)
@@ -963,6 +980,12 @@ struct EagerFocus {
exe: String,
}
#[derive(Parser)]
struct ScrollingLayoutColumns {
/// Desired number of visible columns
count: NonZeroUsize,
}
#[derive(Parser)]
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
struct Opts {
@@ -1182,6 +1205,9 @@ enum SubCommand {
/// Set offsets for a monitor to exclude parts of the work area from tiling
#[clap(arg_required_else_help = true)]
MonitorWorkAreaOffset(MonitorWorkAreaOffset),
/// Set offsets for a workspace to exclude parts of the work area from tiling
#[clap(arg_required_else_help = true)]
WorkspaceWorkAreaOffset(WorkspaceWorkAreaOffset),
/// Toggle application of the window-based work area offset for the focused workspace
ToggleWindowBasedWorkAreaOffset,
/// Set container padding on the focused workspace
@@ -1202,6 +1228,9 @@ enum SubCommand {
/// Cycle between available layouts
#[clap(arg_required_else_help = true)]
CycleLayout(CycleLayout),
/// Set the number of visible columns for the Scrolling layout on the focused workspace
#[clap(arg_required_else_help = true)]
ScrollingLayoutColumns(ScrollingLayoutColumns),
/// Load a custom layout from file for the focused workspace
#[clap(hide = true)]
#[clap(arg_required_else_help = true)]
@@ -1298,7 +1327,7 @@ enum SubCommand {
ToggleWorkspaceFloatOverride,
/// Toggle between the Tiling and Floating layers on the focused workspace
ToggleWorkspaceLayer,
/// Toggle window tiling on the focused workspace
/// Toggle the paused state for all window tiling
TogglePause,
/// Toggle window tiling on the focused workspace
ToggleTiling,
@@ -1552,6 +1581,33 @@ fn main() -> Result<()> {
}
}
SubCommand::Quickstart => {
fn write_file_with_prompt(
path: &PathBuf,
content: &str,
created_files: &mut Vec<String>,
) -> Result<()> {
if path.exists() {
print!(
"{} will be overwritten, do you want to continue? (y/N): ",
path.display()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
if trimmed == "y" || trimmed == "yes" {
std::fs::write(path, content)?;
created_files.push(path.display().to_string());
} else {
println!("Skipping {}", path.display());
}
} else {
std::fs::write(path, content)?;
created_files.push(path.display().to_string());
}
Ok(())
}
let local_appdata_dir = data_local_dir().expect("could not find localdata dir");
let data_dir = local_appdata_dir.join("komorebi");
std::fs::create_dir_all(&*WHKD_CONFIG_DIR)?;
@@ -1567,17 +1623,30 @@ fn main() -> Result<()> {
komorebi_json.replace("Env:USERPROFILE", "Env:KOMOREBI_CONFIG_HOME");
}
std::fs::write(HOME_DIR.join("komorebi.json"), komorebi_json)?;
std::fs::write(HOME_DIR.join("komorebi.bar.json"), komorebi_bar_json)?;
let komorebi_path = HOME_DIR.join("komorebi.json");
let bar_path = HOME_DIR.join("komorebi.bar.json");
let applications_path = HOME_DIR.join("applications.json");
let whkdrc_path = WHKD_CONFIG_DIR.join("whkdrc");
let mut written_files = Vec::new();
write_file_with_prompt(&komorebi_path, &komorebi_json, &mut written_files)?;
write_file_with_prompt(&bar_path, &komorebi_bar_json, &mut written_files)?;
let applications_json = include_str!("../applications.json");
std::fs::write(HOME_DIR.join("applications.json"), applications_json)?;
write_file_with_prompt(&applications_path, applications_json, &mut written_files)?;
let whkdrc = include_str!("../../docs/whkdrc.sample");
std::fs::write(WHKD_CONFIG_DIR.join("whkdrc"), whkdrc)?;
println!("Example komorebi.json, komorebi.bar.json, whkdrc and latest applications.json files created");
println!("You can now run komorebic start --whkd --bar");
write_file_with_prompt(&whkdrc_path, whkdrc, &mut written_files)?;
if written_files.is_empty() {
println!("\nNo files were written.")
} else {
println!(
"\nThe following example files were written:\n{}",
written_files.join("\n")
);
}
println!("\nYou can now run komorebic start --whkd --bar");
}
SubCommand::EnableAutostart(args) => {
let mut current_exe = std::env::current_exe().expect("unable to get exec path");
@@ -1929,6 +1998,20 @@ fn main() -> Result<()> {
bottom: arg.bottom,
}))?;
}
SubCommand::WorkspaceWorkAreaOffset(arg) => {
send_message(&SocketMessage::WorkspaceWorkAreaOffset(
arg.monitor,
arg.workspace,
Rect {
left: arg.left,
top: arg.top,
right: arg.right,
bottom: arg.bottom,
},
))?;
}
SubCommand::ToggleWindowBasedWorkAreaOffset => {
send_message(&SocketMessage::ToggleWindowBasedWorkAreaOffset)?;
}
@@ -2625,6 +2708,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
SubCommand::CycleLayout(arg) => {
send_message(&SocketMessage::CycleLayout(arg.cycle_direction))?;
}
SubCommand::ScrollingLayoutColumns(arg) => {
send_message(&SocketMessage::ScrollingLayoutColumns(arg.count))?;
}
SubCommand::LoadCustomLayout(arg) => {
send_message(&SocketMessage::ChangeLayoutCustom(arg.path))?;
}

View File

@@ -158,6 +158,7 @@ nav:
- cli/invisible-borders.md
- cli/global-work-area-offset.md
- cli/monitor-work-area-offset.md
- cli/workspace-work-area-offset.md
- cli/toggle-window-based-work-area-offset.md
- cli/focused-workspace-container-padding.md
- cli/focused-workspace-padding.md
@@ -165,6 +166,7 @@ nav:
- cli/adjust-workspace-padding.md
- cli/change-layout.md
- cli/cycle-layout.md
- cli/scrolling-layout-columns.md
- cli/flip-layout.md
- cli/promote.md
- cli/promote-focus.md
@@ -253,4 +255,4 @@ nav:
- cli/static-config-schema.md
- cli/generate-static-config.md
- cli/enable-autostart.md
- cli/disable-autostart.md
- cli/disable-autostart.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StaticConfig",
"description": "The `komorebi.json` static configuration file reference for `v0.1.37`",
"description": "The `komorebi.json` static configuration file reference for `v0.1.38`",
"type": "object",
"properties": {
"animation": {
@@ -1790,7 +1790,8 @@
"HorizontalStack",
"UltrawideVerticalStack",
"Grid",
"RightMainVerticalStack"
"RightMainVerticalStack",
"Scrolling"
]
},
"layout_flip": {
@@ -1802,6 +1803,27 @@
"HorizontalAndVertical"
]
},
"layout_options": {
"description": "Layout-specific options (default: None)",
"type": "object",
"properties": {
"scrolling": {
"description": "Options related to the Scrolling layout",
"type": "object",
"required": [
"columns"
],
"properties": {
"columns": {
"description": "Desired number of visible columns (default: 3)",
"type": "integer",
"format": "uint",
"minimum": 0.0
}
}
}
}
},
"layout_rules": {
"description": "Layout rules in the format of threshold => layout (default: None)",
"type": "object",
@@ -1815,7 +1837,8 @@
"HorizontalStack",
"UltrawideVerticalStack",
"Grid",
"RightMainVerticalStack"
"RightMainVerticalStack",
"Scrolling"
]
}
},
@@ -1823,6 +1846,10 @@
"description": "Name",
"type": "string"
},
"tile": {
"description": "Enable or disable tiling for the workspace (default: true)",
"type": "boolean"
},
"wallpaper": {
"description": "Specify a wallpaper for this workspace",
"type": "object",
@@ -2115,6 +2142,38 @@
]
}
},
"work_area_offset": {
"description": "Workspace specific work area offset (default: None)",
"type": "object",
"required": [
"bottom",
"left",
"right",
"top"
],
"properties": {
"bottom": {
"description": "The bottom point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"left": {
"description": "The left point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"right": {
"description": "The right point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"top": {
"description": "The top point in a Win32 Rect",
"type": "integer",
"format": "int32"
}
}
},
"workspace_padding": {
"description": "Workspace padding (default: global)",
"type": "integer",