mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-04-22 08:38:33 +02:00
Compare commits
239 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f669231517 | ||
|
|
4d8afc96c9 | ||
|
|
c154c32b3d | ||
|
|
2618d8f529 | ||
|
|
4dfab7d65f | ||
|
|
075c0602a7 | ||
|
|
83d9232d0b | ||
|
|
84f74fc5a6 | ||
|
|
01b2c52460 | ||
|
|
dae77d87b9 | ||
|
|
07bd53876f | ||
|
|
8c051d9f5e | ||
|
|
7e12f6f4a9 | ||
|
|
67b00fd06d | ||
|
|
ce2c55bbd8 | ||
|
|
ec47526de1 | ||
|
|
81ea4569e1 | ||
|
|
7b3f03bd6a | ||
|
|
a6d46dbf45 | ||
|
|
5b91e22114 | ||
|
|
091e9c3e56 | ||
|
|
a7d29a7344 | ||
|
|
763c710770 | ||
|
|
f8cf70ee1d | ||
|
|
89bd7d2465 | ||
|
|
eec628f7f1 | ||
|
|
77ae5bc2f4 | ||
|
|
87a0aaee0c | ||
|
|
fc5bb892f9 | ||
|
|
26ec574452 | ||
|
|
1c7a5ccb42 | ||
|
|
876439b96b | ||
|
|
d1b1a9e006 | ||
|
|
04791f427b | ||
|
|
e9bccd0316 | ||
|
|
29201b6b94 | ||
|
|
8efce49f2c | ||
|
|
6c022f8d69 | ||
|
|
748659db35 | ||
|
|
91c7f0588c | ||
|
|
60ec439d06 | ||
|
|
ead175ddbc | ||
|
|
5dd3e76602 | ||
|
|
ebcd7ce224 | ||
|
|
b982021573 | ||
|
|
3c84bfd27e | ||
|
|
c874bfc7bf | ||
|
|
85f9c381e5 | ||
|
|
336a4e358f | ||
|
|
4576078b96 | ||
|
|
39971774ea | ||
|
|
7da431081e | ||
|
|
7cc69a4a40 | ||
|
|
5f325a7458 | ||
|
|
34a7b2eb0c | ||
|
|
b08eb0d50c | ||
|
|
005a95b1e6 | ||
|
|
092e36b8b3 | ||
|
|
70be6f4ea4 | ||
|
|
e09d55e71a | ||
|
|
7cef7b53b5 | ||
|
|
8594e72d31 | ||
|
|
bc22ab699f | ||
|
|
b3844af1f3 | ||
|
|
b43f03ce83 | ||
|
|
5c1cfe7b2e | ||
|
|
6269e5972c | ||
|
|
ffa0b0b55e | ||
|
|
66199c5b15 | ||
|
|
aa42a64a48 | ||
|
|
17f1923423 | ||
|
|
5cfc3e831b | ||
|
|
aaf0434053 | ||
|
|
2224479c30 | ||
|
|
957588f60d | ||
|
|
d111d68c0b | ||
|
|
a10b13c799 | ||
|
|
1e69c65c25 | ||
|
|
711ab8d59b | ||
|
|
e1c36c9190 | ||
|
|
686d013734 | ||
|
|
fad4cbf019 | ||
|
|
839f8c9bf7 | ||
|
|
5d468ae70a | ||
|
|
93edcfaa2f | ||
|
|
02a3220cbd | ||
|
|
4b6a7c05e0 | ||
|
|
304158cb1f | ||
|
|
c426c06c01 | ||
|
|
c2cc21d09d | ||
|
|
09a24b89e5 | ||
|
|
4686d5e346 | ||
|
|
532adc9c6c | ||
|
|
a4e8286327 | ||
|
|
75234caa98 | ||
|
|
31b8be1481 | ||
|
|
634bc04d76 | ||
|
|
3b30c10ebb | ||
|
|
3eade94032 | ||
|
|
e46f1f4f6d | ||
|
|
45ea630e6a | ||
|
|
ed01bb674f | ||
|
|
a9534fa49c | ||
|
|
d4c0c35f3a | ||
|
|
f6e0f5ab81 | ||
|
|
51139b9e0c | ||
|
|
d7f1190152 | ||
|
|
b62d77501a | ||
|
|
7cb60ca7c5 | ||
|
|
43edf13bb2 | ||
|
|
cd894655db | ||
|
|
02c54734fb | ||
|
|
4a3f7ee34e | ||
|
|
2db0d888c1 | ||
|
|
cf5a41b5eb | ||
|
|
e4ee298606 | ||
|
|
38c0b25a1c | ||
|
|
d1b6a63af5 | ||
|
|
c246b209c4 | ||
|
|
a2e1b8c967 | ||
|
|
cb387025d2 | ||
|
|
6655d290f2 | ||
|
|
999f2ae2d4 | ||
|
|
cddc69d2bf | ||
|
|
43b2366378 | ||
|
|
e67425f841 | ||
|
|
b2a34204c6 | ||
|
|
d18283969a | ||
|
|
0138a313c0 | ||
|
|
c62ddb3c42 | ||
|
|
87e8eb48a6 | ||
|
|
5f1356b3e2 | ||
|
|
00df672352 | ||
|
|
2b83ff8148 | ||
|
|
749e247d85 | ||
|
|
e70086b681 | ||
|
|
39685dd615 | ||
|
|
228ef78d7f | ||
|
|
e2ae9b1207 | ||
|
|
5e3f1cbb44 | ||
|
|
9fd4dbf044 | ||
|
|
9be248bc03 | ||
|
|
85fe20ebba | ||
|
|
409d374b72 | ||
|
|
1fb0a7cd6e | ||
|
|
d0e46515c5 | ||
|
|
e01bbd9f74 | ||
|
|
be53ea2c24 | ||
|
|
d49279e888 | ||
|
|
daa2912945 | ||
|
|
2c515d54f7 | ||
|
|
f9785bef55 | ||
|
|
0519ebddbf | ||
|
|
b1ca0a3e3c | ||
|
|
84ccfedad4 | ||
|
|
adcb38fed9 | ||
|
|
4a19edaab2 | ||
|
|
676b643faf | ||
|
|
7f74640dbd | ||
|
|
c247426b8e | ||
|
|
4d7ccc5519 | ||
|
|
71e28b33e3 | ||
|
|
40226a2bbd | ||
|
|
2814349228 | ||
|
|
d627a1a771 | ||
|
|
78683ce7b3 | ||
|
|
a1ca4f03c3 | ||
|
|
147a56c274 | ||
|
|
127254b7ac | ||
|
|
4e6e2b3aa8 | ||
|
|
a55069df48 | ||
|
|
7fd545ca35 | ||
|
|
14e63292e1 | ||
|
|
18f34babfa | ||
|
|
2f7ae6f15f | ||
|
|
29a6c39084 | ||
|
|
5d0806a8c9 | ||
|
|
6c53fd7830 | ||
|
|
6ae59671a2 | ||
|
|
f17bfe267e | ||
|
|
840af215a0 | ||
|
|
6981d778a9 | ||
|
|
5d6351f48d | ||
|
|
ac0f33f7ed | ||
|
|
f19bd3032b | ||
|
|
3f3c2815da | ||
|
|
7070878f4a | ||
|
|
d3cb9e07f7 | ||
|
|
6f6181625f | ||
|
|
80dd07fcde | ||
|
|
09d1d69668 | ||
|
|
786f5e846a | ||
|
|
65bc1a966e | ||
|
|
ddafe599a2 | ||
|
|
7ed6df511f | ||
|
|
f9c4dbd447 | ||
|
|
b344888b72 | ||
|
|
a62ed682de | ||
|
|
94e9bb8e9e | ||
|
|
644f7ee604 | ||
|
|
b9a40924a8 | ||
|
|
80bcb51f75 | ||
|
|
e10e11d1de | ||
|
|
2807cafdd0 | ||
|
|
63cf48daa5 | ||
|
|
a2b49845ac | ||
|
|
5b923a135c | ||
|
|
b8a27a93fe | ||
|
|
28a641609c | ||
|
|
f1ee5ea194 | ||
|
|
ff53533da0 | ||
|
|
c4c8bd7d4b | ||
|
|
e1bd0e9fcb | ||
|
|
368d41e3e0 | ||
|
|
2b7c51b87b | ||
|
|
ce3c742e09 | ||
|
|
2a4e6fa6da | ||
|
|
2d19109fb6 | ||
|
|
a9a0ecd49d | ||
|
|
5ec2b80c3a | ||
|
|
4cc059ff1d | ||
|
|
d4d7e2b694 | ||
|
|
84752c4338 | ||
|
|
ffa0786178 | ||
|
|
752dde2288 | ||
|
|
dc771a104d | ||
|
|
e5a7c140ff | ||
|
|
61b231be28 | ||
|
|
cab8b4ad52 | ||
|
|
05777c34b9 | ||
|
|
5094001862 | ||
|
|
bc08e177a1 | ||
|
|
87fe718754 | ||
|
|
fb4fe4d9c3 | ||
|
|
b61b03b1c9 | ||
|
|
a02cd699a0 | ||
|
|
2c876701d8 | ||
|
|
c42739591f | ||
|
|
381253da20 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: LGUG2Z
|
||||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG]: Short descriptive title"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See bug
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots and Videos**
|
||||||
|
Add screenshots and videos to help explain your problem.
|
||||||
|
|
||||||
|
**Operating System**
|
||||||
|
Provide the output of `systeminfo | grep "^OS Name\|^OS Version"`
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
OS Name: Microsoft Windows 11 Pro
|
||||||
|
OS Version: 10.0.22000 N/A Build 22000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
In particular, if you have any other AHK scripts or software running that handle any aspect of window management or manipulation
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[FEAT]: Short descriptive title"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
13
.github/workflows/windows.yaml
vendored
13
.github/workflows/windows.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
target:
|
target:
|
||||||
- x86_64-pc-windows-msvc
|
- x86_64-pc-windows-msvc
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Prep cargo dirs
|
- name: Prep cargo dirs
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
echo "TARGET=${{ matrix.target }}" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
echo "TARGET=${{ matrix.target }}" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
||||||
echo "SKIP_TESTS=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
echo "SKIP_TESTS=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
||||||
- name: Cache cargo registry, git trees and binaries
|
- name: Cache cargo registry, git trees and binaries
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
echo "::set-output name=rust_hash::$(rustc -Vv | grep commit-hash | awk '{print $2}')"
|
echo "::set-output name=rust_hash::$(rustc -Vv | grep commit-hash | awk '{print $2}')"
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Cache cargo build
|
- name: Cache cargo build
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: target
|
path: target
|
||||||
key: ${{ github.base_ref }}-${{ github.head_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ github.base_ref }}-${{ github.head_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
@@ -66,7 +66,6 @@ jobs:
|
|||||||
$ProgressPreference = "SilentlyContinue"
|
$ProgressPreference = "SilentlyContinue"
|
||||||
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
|
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
|
||||||
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --profile=minimal
|
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --profile=minimal
|
||||||
del rustup-init.exe
|
|
||||||
shell: powershell
|
shell: powershell
|
||||||
- name: Ensure stable toolchain is up to date
|
- name: Ensure stable toolchain is up to date
|
||||||
run: rustup update stable
|
run: rustup update stable
|
||||||
@@ -78,12 +77,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cargo build --locked --release --target ${{ matrix.target }}
|
cargo build --locked --release --target ${{ matrix.target }}
|
||||||
- name: Upload the built artifacts
|
- name: Upload the built artifacts
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: komorebi-${{ matrix.target }}
|
name: komorebi-${{ matrix.target }}
|
||||||
path: |
|
path: |
|
||||||
target/${{ matrix.target }}/release/komorebi.exe
|
target/${{ matrix.target }}/release/komorebi.exe
|
||||||
target/${{ matrix.target }}/release/komorebic.exe
|
target/${{ matrix.target }}/release/komorebic.exe
|
||||||
|
target/${{ matrix.target }}/release/komorebi.pdb
|
||||||
|
target/${{ matrix.target }}/release/komorebic.pdb
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
- name: Generate changelog
|
- name: Generate changelog
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
@@ -92,7 +93,7 @@ jobs:
|
|||||||
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
|
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
|
||||||
kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md"
|
kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md"
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v3
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ builds:
|
|||||||
hooks:
|
hooks:
|
||||||
post:
|
post:
|
||||||
- mkdir -p dist/windows_amd64
|
- mkdir -p dist/windows_amd64
|
||||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi.exe" ".\dist\komorebi_windows_amd64\komorebi.exe"
|
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi.exe" ".\dist\komorebi_windows_amd64_v1\komorebi.exe"
|
||||||
- id: komorebic
|
- id: komorebic
|
||||||
main: dummy.go
|
main: dummy.go
|
||||||
goos: ["windows"]
|
goos: ["windows"]
|
||||||
@@ -25,7 +25,7 @@ builds:
|
|||||||
hooks:
|
hooks:
|
||||||
post:
|
post:
|
||||||
- mkdir -p dist/windows_amd64
|
- mkdir -p dist/windows_amd64
|
||||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64\komorebic.exe"
|
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64_v1\komorebic.exe"
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- replacements:
|
- replacements:
|
||||||
@@ -42,18 +42,4 @@ checksum:
|
|||||||
name_template: checksums.txt
|
name_template: checksums.txt
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
|
|
||||||
scoop:
|
|
||||||
bucket:
|
|
||||||
owner: LGUG2Z
|
|
||||||
name: komorebi-bucket
|
|
||||||
token: "{{ .Env.SCOOP_TOKEN }}"
|
|
||||||
homepage: https://github.com/LGUG2Z/komorebi
|
|
||||||
description: A tiling window manager for Windows
|
|
||||||
license: MIT
|
|
||||||
pre_install:
|
|
||||||
- if (Get-Process -Name komorebi -ErrorAction SilentlyContinue) { komorebic stop }
|
|
||||||
post_install:
|
|
||||||
- Write-Host "Run 'cp $original_dir\komorebi.sample.ahk $Env:UserProfile\komorebi.ahk' to get started with the sample configuration"
|
|
||||||
- Write-Host "Once you have a configuration file in place, you can run 'komorebic start' to start the window manager"
|
|
||||||
932
Cargo.lock
generated
932
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
"bindings",
|
"derive-ahk",
|
||||||
"komorebi",
|
"komorebi",
|
||||||
"komorebi-core",
|
"komorebi-core",
|
||||||
"komorebic"
|
"komorebic",
|
||||||
]
|
]
|
||||||
|
|||||||
537
README.md
537
README.md
@@ -2,7 +2,14 @@
|
|||||||
|
|
||||||
Tiling Window Management for Windows.
|
Tiling Window Management for Windows.
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
@@ -14,6 +21,48 @@ _komorebi_ allows you to control application windows, virtual workspaces and dis
|
|||||||
used with third-party software such as [AutoHotKey](https://github.com/Lexikos/AutoHotkey_L) to set user-defined
|
used with third-party software such as [AutoHotKey](https://github.com/Lexikos/AutoHotkey_L) to set user-defined
|
||||||
keyboard shortcuts.
|
keyboard shortcuts.
|
||||||
|
|
||||||
|
Translations of this document can be found in the project wiki:
|
||||||
|
|
||||||
|
- [komorebi 中文用户指南](https://github.com/LGUG2Z/komorebi/wiki/README-zh) (by [@crosstyan](https://github.com/crosstyan))
|
||||||
|
|
||||||
|
There is a [Discord server](https://discord.gg/mGkn66PHkx) available for _komorebi_-related discussion, help,
|
||||||
|
troubleshooting etc. If you have any specific feature requests or bugs to report, please create an issue in this
|
||||||
|
repository.
|
||||||
|
|
||||||
|
Articles, blog posts, demos, and videos about _komorebi_ can be added to this list by PR:
|
||||||
|
|
||||||
|
- [Moving to Windows from Linux Pt 1](https://kvwu.io/posts/moving-to-windows/)
|
||||||
|
- [Windows 下的现代化平铺窗口管理器 komorebi](https://zhuanlan.zhihu.com/p/455064481)
|
||||||
|
|
||||||
|
## GitHub Sponsors Early Access
|
||||||
|
|
||||||
|
[GitHub Sponsors is enabled for this project](https://github.com/sponsors/LGUG2Z). Users who sponsor my work
|
||||||
|
on `komorebi` at any of the predefined monthly tiers will be given access to a private fork of this repository where I
|
||||||
|
push features-in-progress that are not yet quite ready to be pushed on the main repository.
|
||||||
|
|
||||||
|
There will never be any feature of `komorebi` that is gated behind sponsorship; every new feature will always be
|
||||||
|
available for free in the public repository once it meets the requisite level of code quality and completion.
|
||||||
|
|
||||||
|
Features-in-progress that are available in early access will be tagged in the issues with
|
||||||
|
an ["early access" label](https://github.com/LGUG2Z/komorebi/issues?q=is%3Aopen+is%3Aissue+label%3A%22early+access%22).
|
||||||
|
|
||||||
|
## Demonstrations
|
||||||
|
|
||||||
|
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
|
||||||
|
11 with a terminal emulator, a web browser and a code editor. The original
|
||||||
|
video can be viewed
|
||||||
|
[here](https://twitter.com/haxibami/status/1501560766578659332).
|
||||||
|
|
||||||
|
https://user-images.githubusercontent.com/13164844/163496447-20c3ff0a-c5d8-40d1-9cc8-156c4cebf12e.mp4
|
||||||
|
|
||||||
|
[@aik2mlj](https://github.com/aik2mlj) showing _komorebi_ running on Windows 11
|
||||||
|
with multiple workspaces, terminal emulators, a web browser, and the
|
||||||
|
[yasb](https://github.com/DenBot/yasb) status bar with the _komorebi_ workspace
|
||||||
|
widget enabled. The original video can be viewed
|
||||||
|
[here](https://zhuanlan.zhihu.com/p/455064481).
|
||||||
|
|
||||||
|
https://user-images.githubusercontent.com/13164844/163496414-a9cde3d1-b8a7-4a7a-96fb-a8985380bc70.mp4
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
_komorebi_ only responds to [WinEvents](https://docs.microsoft.com/en-us/windows/win32/winauto/event-constants) and the
|
_komorebi_ only responds to [WinEvents](https://docs.microsoft.com/en-us/windows/win32/winauto/event-constants) and the
|
||||||
@@ -75,14 +124,16 @@ PowerShell prompt), and then move the binaries to that directory.
|
|||||||
If you use the [Scoop](https://scoop.sh/) command line installer, you can run the following commands to install the
|
If you use the [Scoop](https://scoop.sh/) command line installer, you can run the following commands to install the
|
||||||
binaries from the latest GitHub Release:
|
binaries from the latest GitHub Release:
|
||||||
|
|
||||||
```
|
```powershell
|
||||||
scoop bucket add komorebi https://github.com/LGUG2Z/komorebi-bucket
|
scoop bucket add extras
|
||||||
scoop install komorebi
|
scoop install komorebi
|
||||||
```
|
```
|
||||||
|
|
||||||
If you install _komorebi_ using Scoop, the binaries will automatically be added to your `Path` and a command will be
|
If you install _komorebi_ using Scoop, the binaries will automatically be added to your `Path` and a command will be
|
||||||
shown for you to run in order to get started using the sample configuration file.
|
shown for you to run in order to get started using the sample configuration file.
|
||||||
|
|
||||||
|
Thanks to [@sitiom](https://github.com/sitiom) for getting _komorebi_ added to the popular Scoop Extras bucket.
|
||||||
|
|
||||||
### Building from Source
|
### Building from Source
|
||||||
|
|
||||||
If you prefer to compile _komorebi_ from source, you will need
|
If you prefer to compile _komorebi_ from source, you will need
|
||||||
@@ -123,8 +174,122 @@ the `AutoHotKey64.exe` executable for AutoHotKey v2 is in your `Path`. If both `
|
|||||||
exist in your home directory, only `komorebi.ahk` will be loaded. An example of an AutoHotKey v2 configuration file
|
exist in your home directory, only `komorebi.ahk` will be loaded. An example of an AutoHotKey v2 configuration file
|
||||||
for _komorebi_ can be found [here](https://gist.github.com/crosstyan/dafacc0778dabf693ce9236c57b201cd).
|
for _komorebi_ can be found [here](https://gist.github.com/crosstyan/dafacc0778dabf693ce9236c57b201cd).
|
||||||
|
|
||||||
|
#### Using Different AHK Executables
|
||||||
|
|
||||||
|
The preferred way to install AutoHotKey for use with `komorebi` is to install it via `scoop`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scoop install autohotkey
|
||||||
|
```
|
||||||
|
|
||||||
|
If you install AutoHotKey using a different method, the name of the executable file may differ from the name given by
|
||||||
|
`scoop`, and thus what is expected by default in `komorebi`.
|
||||||
|
|
||||||
|
You may override the executables that `komorebi` looks for to launch and reload `komorebi.ahk` configuration files using
|
||||||
|
by setting one of the following two environment variables depending on which version of AutoHotKey you wish to use:
|
||||||
|
|
||||||
|
- `$Env:KOMOREBI_AHK_V1_EXE`
|
||||||
|
- `$Env:KOMOREBI_AHK_V2_EXE`
|
||||||
|
|
||||||
|
Please keep in mind that even when setting a custom executable name using these environment variables, the executables
|
||||||
|
are still required to be in your `Path`.
|
||||||
|
|
||||||
### Common First-Time Tips
|
### Common First-Time Tips
|
||||||
|
|
||||||
|
#### Generating Common Application-Specific Configurations
|
||||||
|
|
||||||
|
A curated selection of application-specific configurations can be generated to
|
||||||
|
help ease the setup for first-time users.
|
||||||
|
[`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||||
|
contains YAML definitions of settings that are known to make tricky
|
||||||
|
applications behave as expected. These YAML definitions can be used to generate
|
||||||
|
an AHK file which you can import at the start of your own `komorebi.ahk` file,
|
||||||
|
leaving you to focus primarily on your desired keybindings and workspace
|
||||||
|
configurations.
|
||||||
|
|
||||||
|
If you have settings for an application that you think should be part of this
|
||||||
|
curated selection, please open a PR on the configuration repository.
|
||||||
|
|
||||||
|
In the event that your PR is not accepted, or if you find there are any
|
||||||
|
settings that you wish to override, this can easily be done using an override
|
||||||
|
file.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Clone and enter the repository
|
||||||
|
git clone https://github.com/LGUG2Z/komorebi-application-specific-configuration.git
|
||||||
|
cd komorebi-application-specific-configuration
|
||||||
|
|
||||||
|
# Use komorebic to generate an AHK file
|
||||||
|
komorebic.exe ahk-app-specific-configuration applications.yaml
|
||||||
|
|
||||||
|
# Application-specific generated configuration written to C:\Users\LGUG2Z\.config\komorebi\komorebi.generated.ahk
|
||||||
|
#
|
||||||
|
# You can include the generated configuration at the top of your komorebi.ahk config with this line:
|
||||||
|
#
|
||||||
|
# #Include %A_ScriptDir%\komorebi.generated.ahk
|
||||||
|
|
||||||
|
# Optionally, provide an override file that follows the same schema as the second argument
|
||||||
|
komorebic.exe ahk-app-specific-configuration applications.yaml overrides.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Setting a Custom KOMOREBI_CONFIG_HOME Directory
|
||||||
|
|
||||||
|
If you do not want to keep _komorebi_-related files in your `$Env:UserProfile` directory, you can specify a custom directory
|
||||||
|
by setting the `$Env:KOMOREBI_CONFIG_HOME` environment variable.
|
||||||
|
|
||||||
|
For example, to use the `~/.config/komorebi` directory:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Run this command to make sure that the directory has been created
|
||||||
|
mkdir -p ~/.config/komorebi
|
||||||
|
|
||||||
|
# Run this command to open up your PowerShell profile configuration in Notepad
|
||||||
|
notepad $PROFILE
|
||||||
|
|
||||||
|
# Add this line (with your login user!) to the bottom of your PowerShell profile configuration
|
||||||
|
$Env:KOMOREBI_CONFIG_HOME = 'C:\Users\LGUG2Z\.config\komorebi'
|
||||||
|
|
||||||
|
# Save the changes and then reload the PowerShell profile
|
||||||
|
. $PROFILE
|
||||||
|
```
|
||||||
|
|
||||||
|
If you already have configuration files that you wish to keep, move them to the `~/.config/komorebi` directory.
|
||||||
|
|
||||||
|
The next time you run `komorebic start`, any files created by or loaded by _komorebi_ will be placed or expected to
|
||||||
|
exist in this folder.
|
||||||
|
|
||||||
|
#### Adding an Active Window Border
|
||||||
|
|
||||||
|
If you would like to add a visual border around the currently focused window, two commands are available:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
komorebic.exe active-window-border [enable|disable]
|
||||||
|
komorebic.exe active-window-border-colour [R G B]
|
||||||
|
```
|
||||||
|
|
||||||
|
It is important to note that the active window border will only apply to windows managed by `komorebi`.
|
||||||
|
|
||||||
|
#### Removing Gaps
|
||||||
|
|
||||||
|
If you would like to remove all gaps from a given workspace, both between windows themselves, and between the monitor edges and the windows, you can set the following two configuration options to `0` for the desired monitors and workspaces:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
komorebic.exe container-padding <MONITOR_INDEX> <WORKSPACE_INDEX> 0
|
||||||
|
komorebic.exe workspace padding <MONITOR_INDEX> <WORKSPACE_INDEX> 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Multiple Layout Changes on Startup
|
||||||
|
|
||||||
|
Depending on what is in your configuration, when `komorebi` is started, you may experience the layout rapidly being adjusted
|
||||||
|
with many retile events.
|
||||||
|
|
||||||
|
If you would like to avoid this, you can start `komorebi` with a flag which tells `komorebi` to wait until all configuration
|
||||||
|
has been loaded before listening to and responding to window manager events: `komorebic start --await-configuration`.
|
||||||
|
|
||||||
|
If you start `komorebi` with the `--await-configuration` flag, you _must_ send the `komorebic complete-configuration` command
|
||||||
|
at the end of your `komorebi.ahk` config. The layout will not be updated and `komorebi` will not respond to `komorebic`
|
||||||
|
commands until this command has been received.
|
||||||
|
|
||||||
#### Floating Windows
|
#### Floating Windows
|
||||||
|
|
||||||
Sometimes you will want a specific application to never be tiled, and instead float all the time. You add add rules to
|
Sometimes you will want a specific application to never be tiled, and instead float all the time. You add add rules to
|
||||||
@@ -160,6 +325,150 @@ komorebic.exe identify-tray-application exe Discord.exe
|
|||||||
# komorebic.exe identify-tray-application title [TITLE]
|
# komorebic.exe identify-tray-application title [TITLE]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Microsoft Office Applications
|
||||||
|
|
||||||
|
Microsoft Office applications such as Word and Excel require certain configuration options to be set in order to be
|
||||||
|
managed correctly. Below is an example of configuring Microsoft Word to be managed correctly by _komorebi_.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# This only needs to be added once
|
||||||
|
komorebic.exe float-rule class _WwB
|
||||||
|
|
||||||
|
# Repeat these for other office applications such as EXCEL.EXE etc
|
||||||
|
# Note that the capitalised EXE is important here- double check the
|
||||||
|
# exact case for the name and the file extension in Task Manager or
|
||||||
|
# the AHK Window Spy
|
||||||
|
|
||||||
|
komorebic.exe identify-layered-application exe WINWORD.EXE
|
||||||
|
komorebic.exe identify-border-overflow-application exe WINWORD.EXE
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Focus Follows Mouse
|
||||||
|
|
||||||
|
`komorebi` supports two focus-follows-mouse implementations; the native Windows Xmouse implementation, which treats the
|
||||||
|
desktop, the task bar, and the system tray as windows and switches focus to them eagerly, and a custom `komorebi`
|
||||||
|
implementation, which only considers windows managed by `komorebi` as valid targets to switch focus to when moving the
|
||||||
|
mouse.
|
||||||
|
|
||||||
|
To enable the `komorebi` implementation you must start the process with the `--ffm` flag to explicitly enable the feature.
|
||||||
|
This is because the mouse tracking required for this feature significantly increases the CPU usage of the process (on my
|
||||||
|
machine, it jumps from <1% to ~4~), and this CPU increase persists regardless of whether focus-follows-mouse is enabled
|
||||||
|
or disabled at any given time via `komorebic`'s configuration commands.
|
||||||
|
|
||||||
|
When calling any of the `komorebic` commands related to focus-follows-mouse functionality, the `windows`
|
||||||
|
implementation will be chosen as the default implementation. You can optionally specify the `komorebi` implementation by
|
||||||
|
passing it as an argument to the `--implementation` flag:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
komorebic.exe toggle-focus-follows-mouse --implementation komorebi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mouse Follows Focus
|
||||||
|
|
||||||
|
By default, the mouse will move to the center of the window when the focus is changed in a given direction. This
|
||||||
|
behaviour is know is 'mouse follows focus'. To disable this behaviour across all workspaces, add the following command
|
||||||
|
to your configuration file:
|
||||||
|
|
||||||
|
```ahk
|
||||||
|
Run, komorebic.exe toggle-mouse-follows-focus, , Hide
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Saving and Loading Resized Layouts
|
||||||
|
|
||||||
|
If you create a BSP layout through various resize adjustments that you want to be able to restore easily in the future,
|
||||||
|
it is possible to "quicksave" that layout to the system's temporary folder and load it later in the same session, or
|
||||||
|
alternatively, you may save it to a specific file to be loaded again at any point in the future.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
komorebic.exe quick-save # saves the focused workspace to $Env:TEMP\komorebi.quicksave.json
|
||||||
|
komorebic.exe quick-load # loads $Env:TEMP\komorebi.quicksave.json on the focused workspace
|
||||||
|
|
||||||
|
komorebic.exe save ~/layouts/primary.json # saves the focused workspace to $Env:USERPROFILE\layouts\primary.json
|
||||||
|
komorebic.exe load ~/layouts/secondary.json # loads $Env:USERPROFILE\layouts\secondary.json on the focused workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
These layouts can be applied to arbitrary collections of windows on any workspace, as they only track the layout
|
||||||
|
dimensions and are not coupled to the applications that were running at the time of saving.
|
||||||
|
|
||||||
|
When layouts that expect more or less windows than the number currently on the focused workspace are loaded, `komorebi`
|
||||||
|
will automatically reconcile the difference.
|
||||||
|
|
||||||
|
#### Creating and Loading Custom Layouts
|
||||||
|
|
||||||
|
Particularly for users of ultrawide monitors, traditional tiling layouts may not seem like the most efficient use of
|
||||||
|
screen space. If you feel this is the case with any of the default layouts, you are also welcome to create your own
|
||||||
|
custom layouts and save them as JSON or YAML.
|
||||||
|
|
||||||
|
If you're not comfortable writing the layouts directly in JSON or YAML, you can use
|
||||||
|
the [komorebi Custom Layout Generator](https://lgug2z.github.io/komorebi-custom-layout-generator/) to interactively
|
||||||
|
define a custom layout, and then copy the generated JSON content.
|
||||||
|
|
||||||
|
Custom layouts can be loaded on the current workspace or configured for a specific workspace with the following
|
||||||
|
commands:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
komorebic.exe load-custom-layout ~/custom.yaml
|
||||||
|
komorebic.exe workspace-custom-layout 0 0 ~/custom.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
The fundamental building block of a custom _komorebi_ layout is the Column.
|
||||||
|
|
||||||
|
Columns come in three variants:
|
||||||
|
|
||||||
|
- **Primary**: This is where your primary focus will be on the screen most of the time. There must be exactly one Primary
|
||||||
|
Column in any custom layout. Optionally, you can specify the percentage of the screen width that you want the Primary
|
||||||
|
Column to occupy.
|
||||||
|
- **Secondary**: This is an optional column that can either be full height of split horizontally into a fixed number of
|
||||||
|
maximum rows. There can be any number of Secondary Columns in a custom layout.
|
||||||
|
- **Tertiary**: This is the final column where any remaining windows will be split horizontally into rows as they get added.
|
||||||
|
|
||||||
|
If there is only one window on the screen when a custom layout is selected, that window will take up the full work area
|
||||||
|
of the screen.
|
||||||
|
|
||||||
|
If the number of windows is equal to or less than the total number of columns defined in a custom layout, the windows
|
||||||
|
will be arranged in an equal-width columns.
|
||||||
|
|
||||||
|
When the number of windows is greater than the number of columns defined in the custom layout, the windows will begin to
|
||||||
|
be arranged according to the constraints set on the Primary and Secondary columns of the layout.
|
||||||
|
|
||||||
|
Here is an example custom layout that can be used as a starting point for your own:
|
||||||
|
|
||||||
|
YAML
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- column: Secondary
|
||||||
|
configuration: !Horizontal 2 # max number of rows
|
||||||
|
- column: Primary
|
||||||
|
configuration: !WidthPercentage 50 # percentage of screen
|
||||||
|
- column: Tertiary
|
||||||
|
configuration: Horizontal
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dynamically Changing Layouts Based on Number of Visible Window Containers
|
||||||
|
|
||||||
|
With `komorebi` it is possible to define rules to automatically change the layout on a specified workspace when a
|
||||||
|
threshold of window containers is met.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# On the first workspace of the first monitor (0 0)
|
||||||
|
# When there are one or more window containers visible on the screen (1)
|
||||||
|
# Use the bsp layout (bsp)
|
||||||
|
komorebic workspace-layout-rule 0 0 1 bsp
|
||||||
|
|
||||||
|
# On the first workspace of the first monitor (0 0)
|
||||||
|
# When there are five or more window containers visible on the screen (five)
|
||||||
|
# Use the custom layout stored in the home directory (~/custom.yaml)
|
||||||
|
komorebic workspace-custom-layout-rule 0 0 5 ~/custom.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
However, if you add workspace layout rules, you will not be able to manually change the layout of a workspace until all
|
||||||
|
layout rules for that workspace have been cleared.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# If you decide that workspace layout rules are not for you, you can remove them from that same workspace like this
|
||||||
|
komorebic clear-workspace-layout-rules 0 0
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration with `komorebic`
|
## Configuration with `komorebic`
|
||||||
|
|
||||||
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
|
As previously mentioned, this project does not handle anything related to keybindings and shortcuts directly. I
|
||||||
@@ -171,51 +480,104 @@ keybindings with. You can run `komorebic.exe <COMMAND> --help` to get a full exp
|
|||||||
each command.
|
each command.
|
||||||
|
|
||||||
```
|
```
|
||||||
start Start komorebi.exe as a background process
|
start Start komorebi.exe as a background process
|
||||||
stop Stop the komorebi.exe process and restore all hidden windows
|
stop Stop the komorebi.exe process and restore all hidden windows
|
||||||
state Show a JSON representation of the current window manager state
|
state Show a JSON representation of the current window manager state
|
||||||
log Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
query Query the current window manager state
|
||||||
focus Change focus to the window in the specified direction
|
subscribe Subscribe to komorebi events
|
||||||
move Move the focused window in the specified direction
|
unsubscribe Unsubscribe from komorebi events
|
||||||
stack Stack the focused window in the specified direction
|
log Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||||
resize Resize the focused window in the specified direction
|
quick-save-resize Quicksave the current resize layout dimensions
|
||||||
unstack Unstack the focused window
|
quick-load-resize Load the last quicksaved resize layout dimensions
|
||||||
cycle-stack Cycle the focused stack in the specified cycle direction
|
save-resize Save the current resize layout dimensions to a file
|
||||||
move-to-monitor Move the focused window to the specified monitor
|
load-resize Load the resize layout dimensions from a file
|
||||||
move-to-workspace Move the focused window to the specified workspace
|
focus Change focus to the window in the specified direction
|
||||||
focus-monitor Focus the specified monitor
|
move Move the focused window in the specified direction
|
||||||
focus-workspace Focus the specified workspace on the focused monitor
|
cycle-focus Change focus to the window in the specified cycle direction
|
||||||
new-workspace Create and append a new workspace on the focused monitor
|
cycle-move Move the focused window in the specified cycle direction
|
||||||
adjust-container-padding Adjust container padding on the focused workspace
|
stack Stack the focused window in the specified direction
|
||||||
adjust-workspace-padding Adjust workspace padding on the focused workspace
|
resize-edge Resize the focused window in the specified direction
|
||||||
change-layout Set the layout on the focused workspace
|
resize-axis Resize the focused window or primary column along the specified axis
|
||||||
flip-layout Flip the layout on the focused workspace (BSP only)
|
unstack Unstack the focused window
|
||||||
promote Promote the focused window to the top of the tree
|
cycle-stack Cycle the focused stack in the specified cycle direction
|
||||||
retile Force the retiling of all managed windows
|
move-to-monitor Move the focused window to the specified monitor
|
||||||
ensure-workspaces Create at least this many workspaces for the specified monitor
|
move-to-workspace Move the focused window to the specified workspace
|
||||||
container-padding Set the container padding for the specified workspace
|
send-to-monitor Send the focused window to the specified monitor
|
||||||
workspace-padding Set the workspace padding for the specified workspace
|
send-to-workspace Send the focused window to the specified workspace
|
||||||
workspace-layout Set the layout for the specified workspace
|
send-to-monitor-workspace Send the focused window to the specified monitor workspace
|
||||||
workspace-tiling Enable or disable window tiling for the specified workspace
|
focus-monitor Focus the specified monitor
|
||||||
workspace-name Set the workspace name for the specified workspace
|
focus-workspace Focus the specified workspace on the focused monitor
|
||||||
toggle-pause Toggle the window manager on and off across all monitors
|
focus-monitor-workspace Focus the specified workspace on the target monitor
|
||||||
toggle-tiling Toggle window tiling on the focused workspace
|
cycle-monitor Focus the monitor in the given cycle direction
|
||||||
toggle-float Toggle floating mode for the focused window
|
cycle-workspace Focus the workspace in the given cycle direction
|
||||||
toggle-monocle Toggle monocle mode for the focused container
|
move-workspace-to-monitor Move the focused workspace to the specified monitor
|
||||||
toggle-maximize Toggle native maximization for the focused window
|
new-workspace Create and append a new workspace on the focused monitor
|
||||||
restore-windows Restore all hidden windows (debugging command)
|
resize-delta Set the resize delta (used by resize-edge and resize-axis)
|
||||||
manage Force komorebi to manage the focused window
|
invisible-borders Set the invisible border dimensions around each window
|
||||||
unmanage Unmanage a window that was forcibly managed
|
work-area-offset Set offsets to exclude parts of the work area from tiling
|
||||||
reload-configuration Reload ~/komorebi.ahk (if it exists)
|
adjust-container-padding Adjust container padding on the focused workspace
|
||||||
watch-configuration Toggle the automatic reloading of ~/komorebi.ahk (if it exists)
|
adjust-workspace-padding Adjust workspace padding on the focused workspace
|
||||||
float-rule Add a rule to always float the specified application
|
change-layout Set the layout on the focused workspace
|
||||||
manage-rule Add a rule to always manage the specified application
|
load-custom-layout Load a custom layout from file for the focused workspace
|
||||||
workspace-rule Add a rule to associate an application with a workspace
|
flip-layout Flip the layout on the focused workspace (BSP only)
|
||||||
identify-tray-application Identify an application that closes to the system tray
|
promote Promote the focused window to the top of the tree
|
||||||
focus-follows-mouse Enable or disable focus follows mouse for the operating system
|
retile Force the retiling of all managed windows
|
||||||
help Print this message or the help of the given subcommand(s)
|
ensure-workspaces Create at least this many workspaces for the specified monitor
|
||||||
|
container-padding Set the container padding for the specified workspace
|
||||||
|
workspace-padding Set the workspace padding for the specified workspace
|
||||||
|
workspace-layout Set the layout for the specified workspace
|
||||||
|
workspace-custom-layout Set a custom layout for the specified workspace
|
||||||
|
workspace-layout-rule Add a dynamic layout rule for the specified workspace
|
||||||
|
workspace-custom-layout-rule Add a dynamic custom layout for the specified workspace
|
||||||
|
clear-workspace-layout-rules Clear all dynamic layout rules for the specified workspace
|
||||||
|
workspace-tiling Enable or disable window tiling for the specified workspace
|
||||||
|
workspace-name Set the workspace name for the specified workspace
|
||||||
|
toggle-window-container-behaviour Toggle the behaviour for new windows (stacking or dynamic tiling)
|
||||||
|
toggle-pause Toggle window tiling on the focused workspace
|
||||||
|
toggle-tiling Toggle window tiling on the focused workspace
|
||||||
|
toggle-float Toggle floating mode for the focused window
|
||||||
|
toggle-monocle Toggle monocle mode for the focused container
|
||||||
|
toggle-maximize Toggle native maximization for the focused window
|
||||||
|
restore-windows Restore all hidden windows (debugging command)
|
||||||
|
manage Force komorebi to manage the focused window
|
||||||
|
unmanage Unmanage a window that was forcibly managed
|
||||||
|
reload-configuration Reload ~/komorebi.ahk (if it exists)
|
||||||
|
watch-configuration Enable or disable watching of ~/komorebi.ahk (if it exists)
|
||||||
|
complete-configuration Signal that the final configuration option has been sent
|
||||||
|
window-hiding-behaviour Set the window behaviour when switching workspaces / cycling stacks
|
||||||
|
cross-monitor-move-behaviour Set the behaviour when moving windows across monitor boundaries
|
||||||
|
toggle-cross-monitor-move-behaviour Toggle the behaviour when moving windows across monitor boundaries
|
||||||
|
unmanaged-window-operation-behaviour Set the operation behaviour when the focused window is not managed
|
||||||
|
float-rule Add a rule to always float the specified application
|
||||||
|
manage-rule Add a rule to always manage the specified application
|
||||||
|
workspace-rule Add a rule to associate an application with a workspace
|
||||||
|
identify-object-name-change-application Identify an application that sends EVENT_OBJECT_NAMECHANGE on launch
|
||||||
|
identify-tray-application Identify an application that closes to the system tray
|
||||||
|
identify-layered-application Identify an application that has WS_EX_LAYERED, but should still be managed
|
||||||
|
identify-border-overflow-application Identify an application that has overflowing borders
|
||||||
|
active-window-border Enable or disable the active window border
|
||||||
|
active-window-border-colour Set the colour for the active window border
|
||||||
|
focus-follows-mouse Enable or disable focus follows mouse for the operating system
|
||||||
|
toggle-focus-follows-mouse Toggle focus follows mouse for the operating system
|
||||||
|
mouse-follows-focus Enable or disable mouse follows focus on all workspaces
|
||||||
|
toggle-mouse-follows-focus Toggle mouse follows focus on all workspaces
|
||||||
|
ahk-library Generate a library of AutoHotKey helper functions
|
||||||
|
ahk-app-specific-configuration Generate common app-specific configurations and fixes to use in komorebi.ahk
|
||||||
|
format-app-specific-configuration Format a YAML file for use with the 'ahk-app-specific-configuration' command
|
||||||
|
notification-schema Generate a JSON Schema of subscription notifications
|
||||||
|
help Print this message or the help of the given subcommand(s)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### AutoHotKey Helper Library for `komorebic`
|
||||||
|
|
||||||
|
Additionally, you may run `komorebic.exe ahk-library` to
|
||||||
|
generate [a helper library for AutoHotKey](komorebic.lib.sample.ahk) which wraps every `komorebic` command in a native
|
||||||
|
AHK function.
|
||||||
|
|
||||||
|
If you include the generated library at the top of your `~/komorebi.ahk` configuration file, you will be able to call
|
||||||
|
any of the functions that it contains. A sample AHK script that shows how this library can be
|
||||||
|
used [is available here](komorebi.sample.with.lib.ahk).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] Multi-monitor
|
- [x] Multi-monitor
|
||||||
@@ -223,32 +585,55 @@ help Print this message or the help of the given subcomm
|
|||||||
- [x] Window stacks
|
- [x] Window stacks
|
||||||
- [x] Cycle through stacked windows
|
- [x] Cycle through stacked windows
|
||||||
- [x] Change focused window by direction
|
- [x] Change focused window by direction
|
||||||
|
- [x] Change focused window by direction across monitor boundary
|
||||||
- [x] Move focused window container in direction
|
- [x] Move focused window container in direction
|
||||||
- [x] Move focused window container to monitor
|
- [x] Move focused window container in direction across monitor boundary
|
||||||
- [x] Move focused window container to workspace
|
- [x] Move focused window container to monitor and follow
|
||||||
|
- [x] Move focused window container to workspace follow
|
||||||
|
- [x] Send focused window container to monitor
|
||||||
|
- [x] Send focused window container to workspace
|
||||||
|
- [x] Move focused workspace to monitor
|
||||||
- [x] Mouse follows focused container
|
- [x] Mouse follows focused container
|
||||||
- [x] Resize window container in direction
|
- [x] Resize window container in direction
|
||||||
|
- [x] Resize window container on axis
|
||||||
|
- [x] Set custom resize delta
|
||||||
- [ ] Resize child window containers by split ratio
|
- [ ] Resize child window containers by split ratio
|
||||||
|
- [x] Quicksave and quickload layouts with resize dimensions
|
||||||
|
- [x] Save and load layouts with resize dimensions to/from specific files
|
||||||
- [x] Mouse drag to swap window container position
|
- [x] Mouse drag to swap window container position
|
||||||
- [x] Mouse drag to resize window container
|
- [x] Mouse drag to resize window container
|
||||||
- [x] Configurable workspace and container gaps
|
- [x] Configurable workspace and container gaps
|
||||||
- [x] BSP tree layout
|
- [x] BSP tree layout (`bsp`)
|
||||||
- [x] Flip BSP tree layout horizontally or vertically
|
- [x] Flip BSP tree layout horizontally or vertically
|
||||||
- [x] Equal-width, max-height column layout
|
- [x] Equal-width, max-height column layout (`columns`)
|
||||||
|
- [x] Equal-height, max-width row layout (`rows`)
|
||||||
|
- [x] Main half-height window with vertical stack layout (`horizontal-stack`)
|
||||||
|
- [x] Main half-width window with horizontal stack layout (`vertical-stack`)
|
||||||
|
- [x] 2x Main window (half and quarter-width) with horizontal stack layout (`ultrawide-vertical-stack`)
|
||||||
|
- [x] Load custom layouts from JSON and YAML representations
|
||||||
|
- [x] Dynamically select layout based on the number of open windows
|
||||||
- [x] Floating rules based on exe name, window title and class
|
- [x] Floating rules based on exe name, window title and class
|
||||||
- [x] Workspace rules based on exe name and window class
|
- [x] Workspace rules based on exe name and window class
|
||||||
- [x] Additional manage rules based on exe name and window class
|
- [x] Additional manage rules based on exe name and window class
|
||||||
|
- [x] Identify applications which overflow their borders by exe name and class
|
||||||
- [x] Identify 'close/minimize to tray' applications by exe name and class
|
- [x] Identify 'close/minimize to tray' applications by exe name and class
|
||||||
|
- [x] Configure work area offsets to preserve space for custom taskbars
|
||||||
|
- [x] Configure and compensate for the size of Windows 10's invisible borders
|
||||||
- [x] Toggle floating windows
|
- [x] Toggle floating windows
|
||||||
- [x] Toggle monocle window
|
- [x] Toggle monocle window
|
||||||
- [x] Toggle native maximization
|
- [x] Toggle native maximization
|
||||||
- [x] Toggle focus follows mouse
|
- [x] Toggle mouse follows focus
|
||||||
|
- [x] Toggle Xmouse/Windows focus follows mouse implementation
|
||||||
|
- [x] Toggle Komorebi focus follows mouse implementation (desktop and system tray-aware)
|
||||||
- [x] Toggle automatic tiling
|
- [x] Toggle automatic tiling
|
||||||
- [x] Pause all window management
|
- [x] Pause all window management
|
||||||
- [x] Load configuration on startup
|
- [x] Load configuration on startup
|
||||||
- [x] Manually reload configuration
|
- [x] Manually reload configuration
|
||||||
- [x] Watch configuration for changes
|
- [x] Watch configuration for changes
|
||||||
|
- [x] Helper library for AutoHotKey
|
||||||
- [x] View window manager state
|
- [x] View window manager state
|
||||||
|
- [x] Query window manager state
|
||||||
|
- [x] Subscribe to event and message notifications
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -275,14 +660,14 @@ the IDE for completions and navigation:
|
|||||||
|
|
||||||
## Logs and Debugging
|
## Logs and Debugging
|
||||||
|
|
||||||
Logs from `komorebi` will be appended to `~/komorebi.log`; this file is never rotated or overwritten, so it will keep
|
Logs from `komorebi` will be appended to `%LOCALAPPDATA%/komorebi/komorebi.log`; this file is never rotated or overwritten, so it will keep
|
||||||
growing until it is deleted by the user.
|
growing until it is deleted by the user.
|
||||||
|
|
||||||
Whenever running the `komorebic stop` command or sending a Ctrl-C signal to `komorebi` directly, the `komorebi` process
|
Whenever running the `komorebic stop` command or sending a Ctrl-C signal to `komorebi` directly, the `komorebi` process
|
||||||
ensures that all hidden windows are restored before termination.
|
ensures that all hidden windows are restored before termination.
|
||||||
|
|
||||||
If however, you ever end up with windows that are hidden and cannot be restored, a list of window handles known
|
If however, you ever end up with windows that are hidden and cannot be restored, a list of window handles known
|
||||||
to `komorebi` are stored and continuously updated in `~/komorebi.hwnd.json`.
|
to `komorebi` are stored and continuously updated in `%LOCALAPPDATA%/komorebi//komorebi.hwnd.json`.
|
||||||
|
|
||||||
### Restoring Windows
|
### Restoring Windows
|
||||||
|
|
||||||
@@ -297,8 +682,7 @@ process and the log will appear frozen.
|
|||||||
|
|
||||||
If you believe you have encountered a deadlock, you can compile `komorebi` with `--features deadlock_detection` and try
|
If you believe you have encountered a deadlock, you can compile `komorebi` with `--features deadlock_detection` and try
|
||||||
reproducing the deadlock again. This will check for deadlocks every 5 seconds in the background, and if a deadlock is
|
reproducing the deadlock again. This will check for deadlocks every 5 seconds in the background, and if a deadlock is
|
||||||
found, information about it will appear in the log which can be shared when opening an issu which can be shared when
|
found, information about it will appear in the log which can be shared when opening an issue.
|
||||||
opening an issue.
|
|
||||||
|
|
||||||
## Window Manager State and Integrations
|
## Window Manager State and Integrations
|
||||||
|
|
||||||
@@ -307,3 +691,46 @@ representation of the `State` struct, which includes the current state of `Windo
|
|||||||
|
|
||||||
This may also be polled to build further integrations and widgets on top of (if you ever wanted to build something
|
This may also be polled to build further integrations and widgets on top of (if you ever wanted to build something
|
||||||
like [Stackline](https://github.com/AdamWagner/stackline) for Windows, you could do it by polling this command).
|
like [Stackline](https://github.com/AdamWagner/stackline) for Windows, you could do it by polling this command).
|
||||||
|
|
||||||
|
## Window Manager Event Subscriptions
|
||||||
|
|
||||||
|
It is also possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
|
||||||
|
by `komorebi` using [Named Pipes](https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes).
|
||||||
|
|
||||||
|
First, your application must create a named pipe. Once the named pipe has been created, run the following command:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
komorebic.exe subscribe <your pipe name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that you do not have to include the full path of the named pipe, just the name.
|
||||||
|
|
||||||
|
If the named pipe exists, `komorebi` will start pushing JSON data of successfully handled events and messages:
|
||||||
|
|
||||||
|
```json lines
|
||||||
|
{"event":{"type":"AddSubscriber","content":"yasb"},"state":{}}
|
||||||
|
{"event":{"type":"FocusWindow","content":"Left"},"state":{}}
|
||||||
|
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":131444,"title":"komorebi – README.md","exe":"idea64.exe","class":"SunAwtFrame","rect":{"left":13,"top":60,"right":1520,"bottom":1655}}]},"state":{}}
|
||||||
|
{"event":{"type":"MonitorPoll","content":["ObjectCreate",{"hwnd":5572450,"title":"OLEChannelWnd","exe":"explorer.exe","class":"OleMainThreadWndClass","rect":{"left":0,"top":0,"right":0,"bottom":0}}]},"state":{}}
|
||||||
|
{"event":{"type":"FocusWindow","content":"Right"},"state":{}}
|
||||||
|
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":132968,"title":"Windows PowerShell","exe":"WindowsTerminal.exe","class":"CASCADIA_HOSTING_WINDOW_CLASS","rect":{"left":1539,"top":60,"right":1520,"bottom":821}}]},"state":{}}
|
||||||
|
{"event":{"type":"FocusWindow","content":"Down"},"state":{}}
|
||||||
|
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":329264,"title":"den — Mozilla Firefox","exe":"firefox.exe","class":"MozillaWindowClass","rect":{"left":1539,"top":894,"right":1520,"bottom":821}}]},"state":{}}
|
||||||
|
{"event":{"type":"FocusWindow","content":"Up"},"state":{}}
|
||||||
|
{"event":{"type":"FocusChange","content":["SystemForeground",{"hwnd":132968,"title":"Windows PowerShell","exe":"WindowsTerminal.exe","class":"CASCADIA_HOSTING_WINDOW_CLASS","rect":{"left":1539,"top":60,"right":1520,"bottom":821}}]},"state":{}}
|
||||||
|
```
|
||||||
|
|
||||||
|
You may then filter on the `type` key to listen to the events that you are interested in. For a full list of possible
|
||||||
|
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
|
||||||
|
in `komorebi-core`.
|
||||||
|
|
||||||
|
An example of how to create a named pipe and a subscription to `komorebi`'s handled events in Python
|
||||||
|
by [@denBot](https://github.com/denBot) can be
|
||||||
|
found [here](https://gist.github.com/denBot/4136279812f87819f86d99eba77c1ee0).
|
||||||
|
|
||||||
|
### Subscription Event Notification Schema
|
||||||
|
|
||||||
|
A [JSON Schema](https://json-schema.org/) of the event notifications emitted to subscribers can be generated with
|
||||||
|
the `komorebic notification-schema` command. The output of this command can be redirected to the clipboard or a file,
|
||||||
|
which can be used with services such as [Quicktype](https://app.quicktype.io/) to generate type definitions in different
|
||||||
|
programming languages.
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
windows::build!(
|
|
||||||
Windows::Win32::Foundation::RECT,
|
|
||||||
Windows::Win32::Foundation::POINT,
|
|
||||||
Windows::Win32::Foundation::BOOL,
|
|
||||||
Windows::Win32::Foundation::PWSTR,
|
|
||||||
Windows::Win32::Foundation::HWND,
|
|
||||||
Windows::Win32::Foundation::LPARAM,
|
|
||||||
// error: `Windows.Win32.Graphics.Dwm.DWMWA_CLOAKED` not found in metadata
|
|
||||||
Windows::Win32::Graphics::Dwm::*,
|
|
||||||
// error: `Windows.Win32.Graphics.Gdi.MONITOR_DEFAULTTONEAREST` not found in metadata
|
|
||||||
Windows::Win32::Graphics::Gdi::*,
|
|
||||||
Windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS,
|
|
||||||
Windows::Win32::System::Threading::PROCESS_NAME_FORMAT,
|
|
||||||
Windows::Win32::System::Threading::OpenProcess,
|
|
||||||
Windows::Win32::System::Threading::QueryFullProcessImageNameW,
|
|
||||||
Windows::Win32::System::Threading::GetCurrentThreadId,
|
|
||||||
Windows::Win32::System::Threading::AttachThreadInput,
|
|
||||||
Windows::Win32::System::Threading::GetCurrentProcessId,
|
|
||||||
Windows::Win32::UI::KeyboardAndMouseInput::SetFocus,
|
|
||||||
Windows::Win32::UI::Accessibility::SetWinEventHook,
|
|
||||||
Windows::Win32::UI::Accessibility::HWINEVENTHOOK,
|
|
||||||
// error: `Windows.Win32.UI.WindowsAndMessaging.GWL_EXSTYLE` not found in metadata
|
|
||||||
Windows::Win32::UI::WindowsAndMessaging::*,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
::windows::include_bindings!();
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bindings"
|
name = "derive-ahk"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Jade Iqbal"]
|
edition = "2021"
|
||||||
edition = "2018"
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
windows = "0.18"
|
proc-macro2 = "1.0"
|
||||||
|
syn = "1.0"
|
||||||
[build-dependencies]
|
quote = "1.0"
|
||||||
windows = "0.18"
|
|
||||||
212
derive-ahk/src/lib.rs
Normal file
212
derive-ahk/src/lib.rs
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||||
|
#![allow(clippy::missing_errors_doc)]
|
||||||
|
#![no_implicit_prelude]
|
||||||
|
|
||||||
|
use ::std::clone::Clone;
|
||||||
|
use ::std::convert::From;
|
||||||
|
use ::std::convert::Into;
|
||||||
|
use ::std::iter::Extend;
|
||||||
|
use ::std::iter::Iterator;
|
||||||
|
use ::std::matches;
|
||||||
|
use ::std::option::Option::Some;
|
||||||
|
use ::std::string::ToString;
|
||||||
|
use ::std::unreachable;
|
||||||
|
|
||||||
|
use ::quote::quote;
|
||||||
|
use ::syn::parse_macro_input;
|
||||||
|
use ::syn::Data;
|
||||||
|
use ::syn::DataEnum;
|
||||||
|
use ::syn::DeriveInput;
|
||||||
|
use ::syn::Fields;
|
||||||
|
use ::syn::FieldsNamed;
|
||||||
|
use ::syn::FieldsUnnamed;
|
||||||
|
use ::syn::Meta;
|
||||||
|
use ::syn::NestedMeta;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
#[proc_macro_derive(AhkFunction)]
|
||||||
|
pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
let name = input.ident;
|
||||||
|
|
||||||
|
match input.data {
|
||||||
|
Data::Struct(s) => match s.fields {
|
||||||
|
Fields::Named(FieldsNamed { named, .. }) => {
|
||||||
|
let argument_idents = named
|
||||||
|
.iter()
|
||||||
|
// Filter out the flags
|
||||||
|
.filter(|&f| {
|
||||||
|
let mut include = true;
|
||||||
|
for attribute in &f.attrs {
|
||||||
|
if let ::std::result::Result::Ok(Meta::List(list)) =
|
||||||
|
attribute.parse_meta()
|
||||||
|
{
|
||||||
|
for nested in list.nested {
|
||||||
|
if let NestedMeta::Meta(Meta::Path(path)) = nested {
|
||||||
|
if path.is_ident("long") {
|
||||||
|
include = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
include
|
||||||
|
})
|
||||||
|
.map(|f| &f.ident);
|
||||||
|
|
||||||
|
let argument_idents_clone = argument_idents.clone();
|
||||||
|
|
||||||
|
let called_arguments = quote! {#(%#argument_idents_clone%) *}
|
||||||
|
.to_string()
|
||||||
|
.replace(" %", "%")
|
||||||
|
.replace("% ", "%")
|
||||||
|
.replace("%%", "% %");
|
||||||
|
|
||||||
|
let flag_idents = named
|
||||||
|
.iter()
|
||||||
|
// Filter only the flags
|
||||||
|
.filter(|f| {
|
||||||
|
let mut include = false;
|
||||||
|
|
||||||
|
for attribute in &f.attrs {
|
||||||
|
if let ::std::result::Result::Ok(Meta::List(list)) =
|
||||||
|
attribute.parse_meta()
|
||||||
|
{
|
||||||
|
for nested in list.nested {
|
||||||
|
if let NestedMeta::Meta(Meta::Path(path)) = nested {
|
||||||
|
// Identify them using the --long flag name
|
||||||
|
if path.is_ident("long") {
|
||||||
|
include = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
include
|
||||||
|
})
|
||||||
|
.map(|f| &f.ident);
|
||||||
|
|
||||||
|
let has_flags = flag_idents.clone().count() != 0;
|
||||||
|
|
||||||
|
if has_flags {
|
||||||
|
let flag_idents_concat = flag_idents.clone();
|
||||||
|
let argument_idents_concat = argument_idents.clone();
|
||||||
|
|
||||||
|
// Concat the args and flag args if there are flags
|
||||||
|
let all_arguments =
|
||||||
|
quote! {#(#argument_idents_concat,) * #(#flag_idents_concat), *}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let flag_idents_clone = flag_idents.clone();
|
||||||
|
let flags = quote! {#(--#flag_idents_clone) *}
|
||||||
|
.to_string()
|
||||||
|
.replace("- - ", "--");
|
||||||
|
|
||||||
|
let called_flag_arguments = quote! {#(%#flag_idents%) *}
|
||||||
|
.to_string()
|
||||||
|
.replace(" %", "%")
|
||||||
|
.replace("% ", "%")
|
||||||
|
.replace("%%", "% %");
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl AhkFunction for #name {
|
||||||
|
fn generate_ahk_function() -> String {
|
||||||
|
::std::format!(r#"
|
||||||
|
{}({}) {{
|
||||||
|
Run, komorebic.exe {} {} {} {}, , Hide
|
||||||
|
}}"#,
|
||||||
|
::std::stringify!(#name),
|
||||||
|
#all_arguments,
|
||||||
|
::std::stringify!(#name).to_kebab_case(),
|
||||||
|
#called_arguments,
|
||||||
|
#flags,
|
||||||
|
#called_flag_arguments
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let arguments = quote! {#(#argument_idents), *}.to_string();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl AhkFunction for #name {
|
||||||
|
fn generate_ahk_function() -> String {
|
||||||
|
::std::format!(r#"
|
||||||
|
{}({}) {{
|
||||||
|
Run, komorebic.exe {} {}, , Hide
|
||||||
|
}}"#,
|
||||||
|
::std::stringify!(#name),
|
||||||
|
#arguments,
|
||||||
|
::std::stringify!(#name).to_kebab_case(),
|
||||||
|
#called_arguments
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!("only to be used on structs with named fields"),
|
||||||
|
},
|
||||||
|
_ => unreachable!("only to be used on structs"),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(AhkLibrary)]
|
||||||
|
pub fn ahk_library(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
let name = input.ident;
|
||||||
|
|
||||||
|
match input.data {
|
||||||
|
Data::Enum(DataEnum { variants, .. }) => {
|
||||||
|
let enums = variants.iter().filter(|&v| {
|
||||||
|
matches!(v.fields, Fields::Unit) || matches!(v.fields, Fields::Unnamed(..))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut stream = ::proc_macro2::TokenStream::new();
|
||||||
|
|
||||||
|
for variant in enums.clone() {
|
||||||
|
match &variant.fields {
|
||||||
|
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
|
||||||
|
for field in unnamed {
|
||||||
|
stream.extend(quote! {
|
||||||
|
v.push(#field::generate_ahk_function());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Fields::Unit => {
|
||||||
|
let name = &variant.ident;
|
||||||
|
stream.extend(quote! {
|
||||||
|
v.push(::std::format!(r#"
|
||||||
|
{}() {{
|
||||||
|
Run, komorebic.exe {}, , Hide
|
||||||
|
}}"#,
|
||||||
|
::std::stringify!(#name),
|
||||||
|
::std::stringify!(#name).to_kebab_case()
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Fields::Named(_) => {
|
||||||
|
unreachable!("only to be used with unnamed and unit fields");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl #name {
|
||||||
|
fn generate_ahk_library() -> String {
|
||||||
|
let mut v: Vec<String> = vec![String::from("; Generated by komorebic.exe")];
|
||||||
|
|
||||||
|
#stream
|
||||||
|
|
||||||
|
v.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!("only to be used on enums"),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
42
justfile
Normal file
42
justfile
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
set shell := ["cmd.exe", "/C"]
|
||||||
|
export RUST_BACKTRACE := "full"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
cargo clean
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
cargo +nightly fmt
|
||||||
|
cargo +stable clippy
|
||||||
|
prettier --write README.md
|
||||||
|
|
||||||
|
install-komorebic:
|
||||||
|
cargo +stable install --path komorebic --locked
|
||||||
|
|
||||||
|
install-komorebi:
|
||||||
|
cargo +stable install --path komorebi --locked
|
||||||
|
|
||||||
|
install:
|
||||||
|
just install-komorebic
|
||||||
|
just install-komorebi
|
||||||
|
komorebic ahk-library
|
||||||
|
cat '%USERPROFILE%\.config\komorebi\komorebic.lib.ahk' > komorebic.lib.sample.ahk
|
||||||
|
|
||||||
|
run:
|
||||||
|
just install-komorebic
|
||||||
|
cargo +stable run --bin komorebi --locked
|
||||||
|
|
||||||
|
warn $RUST_LOG="warn":
|
||||||
|
just run
|
||||||
|
|
||||||
|
info $RUST_LOG="info":
|
||||||
|
just run
|
||||||
|
|
||||||
|
debug $RUST_LOG="debug":
|
||||||
|
just run
|
||||||
|
|
||||||
|
trace $RUST_LOG="trace":
|
||||||
|
just run
|
||||||
|
|
||||||
|
deadlock $RUST_LOG="trace":
|
||||||
|
just install-komorebic
|
||||||
|
cargo +stable run --bin komorebi --locked --features deadlock_detection
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi-core"
|
name = "komorebi-core"
|
||||||
version = "0.1.1"
|
version = "0.1.11"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bindings = { package = "bindings", path = "../bindings" }
|
clap = { version = "3", features = ["derive"] }
|
||||||
|
color-eyre = "0.6"
|
||||||
clap = "3.0.0-beta.4"
|
|
||||||
color-eyre = "0.5"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
strum = { version = "0.21", features = ["derive"] }
|
serde_yaml = "0.9"
|
||||||
|
strum = { version = "0.24", features = ["derive"] }
|
||||||
|
schemars = "0.8"
|
||||||
|
|
||||||
|
[dependencies.windows]
|
||||||
|
version = "0.39"
|
||||||
|
features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
]
|
||||||
|
|||||||
586
komorebi-core/src/arrangement.rs
Normal file
586
komorebi-core/src/arrangement.rs
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
use clap::ArgEnum;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use strum::Display;
|
||||||
|
use strum::EnumString;
|
||||||
|
|
||||||
|
use crate::custom_layout::Column;
|
||||||
|
use crate::custom_layout::ColumnSplit;
|
||||||
|
use crate::custom_layout::ColumnSplitWithCapacity;
|
||||||
|
use crate::CustomLayout;
|
||||||
|
use crate::DefaultLayout;
|
||||||
|
use crate::Rect;
|
||||||
|
|
||||||
|
pub trait Arrangement {
|
||||||
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
area: &Rect,
|
||||||
|
len: NonZeroUsize,
|
||||||
|
container_padding: Option<i32>,
|
||||||
|
layout_flip: Option<Axis>,
|
||||||
|
resize_dimensions: &[Option<Rect>],
|
||||||
|
) -> Vec<Rect>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Arrangement for DefaultLayout {
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
area: &Rect,
|
||||||
|
len: NonZeroUsize,
|
||||||
|
container_padding: Option<i32>,
|
||||||
|
layout_flip: Option<Axis>,
|
||||||
|
resize_dimensions: &[Option<Rect>],
|
||||||
|
) -> Vec<Rect> {
|
||||||
|
let len = usize::from(len);
|
||||||
|
let mut dimensions = match self {
|
||||||
|
Self::BSP => recursive_fibonacci(
|
||||||
|
0,
|
||||||
|
len,
|
||||||
|
area,
|
||||||
|
layout_flip,
|
||||||
|
calculate_resize_adjustments(resize_dimensions),
|
||||||
|
),
|
||||||
|
Self::Columns => columns(area, len),
|
||||||
|
Self::Rows => rows(area, len),
|
||||||
|
Self::VerticalStack => {
|
||||||
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
|
|
||||||
|
let primary_right = match len {
|
||||||
|
1 => area.right,
|
||||||
|
_ => area.right / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut main_left = area.left;
|
||||||
|
let mut stack_left = area.left + primary_right;
|
||||||
|
|
||||||
|
match layout_flip {
|
||||||
|
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
|
||||||
|
main_left = main_left + area.right - primary_right;
|
||||||
|
stack_left = area.left;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len >= 1 {
|
||||||
|
layouts.push(Rect {
|
||||||
|
left: main_left,
|
||||||
|
top: area.top,
|
||||||
|
right: primary_right,
|
||||||
|
bottom: area.bottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
if len > 1 {
|
||||||
|
layouts.append(&mut rows(
|
||||||
|
&Rect {
|
||||||
|
left: stack_left,
|
||||||
|
top: area.top,
|
||||||
|
right: area.right - primary_right,
|
||||||
|
bottom: area.bottom,
|
||||||
|
},
|
||||||
|
len - 1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layouts
|
||||||
|
}
|
||||||
|
Self::HorizontalStack => {
|
||||||
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
|
|
||||||
|
let bottom = match len {
|
||||||
|
1 => area.bottom,
|
||||||
|
_ => area.bottom / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut main_top = area.top;
|
||||||
|
let mut stack_top = area.top + bottom;
|
||||||
|
|
||||||
|
match layout_flip {
|
||||||
|
Some(Axis::Vertical | Axis::HorizontalAndVertical) if len > 1 => {
|
||||||
|
main_top = main_top + area.bottom - bottom;
|
||||||
|
stack_top = area.top;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len >= 1 {
|
||||||
|
layouts.push(Rect {
|
||||||
|
left: area.left,
|
||||||
|
top: main_top,
|
||||||
|
right: area.right,
|
||||||
|
bottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
if len > 1 {
|
||||||
|
layouts.append(&mut columns(
|
||||||
|
&Rect {
|
||||||
|
left: area.left,
|
||||||
|
top: stack_top,
|
||||||
|
right: area.right,
|
||||||
|
bottom: area.bottom - bottom,
|
||||||
|
},
|
||||||
|
len - 1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layouts
|
||||||
|
}
|
||||||
|
Self::UltrawideVerticalStack => {
|
||||||
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
|
|
||||||
|
let primary_right = match len {
|
||||||
|
1 => area.right,
|
||||||
|
_ => area.right / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let secondary_right = match len {
|
||||||
|
1 => 0,
|
||||||
|
2 => area.right - primary_right,
|
||||||
|
_ => (area.right - primary_right) / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (primary_left, secondary_left, stack_left) = match len {
|
||||||
|
1 => (area.left, 0, 0),
|
||||||
|
2 => {
|
||||||
|
let mut primary = area.left + secondary_right;
|
||||||
|
let mut secondary = area.left;
|
||||||
|
|
||||||
|
match layout_flip {
|
||||||
|
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
|
||||||
|
primary = area.left;
|
||||||
|
secondary = area.left + primary_right;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
(primary, secondary, 0)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let primary = area.left + secondary_right;
|
||||||
|
let mut secondary = area.left;
|
||||||
|
let mut stack = area.left + primary_right + secondary_right;
|
||||||
|
|
||||||
|
match layout_flip {
|
||||||
|
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
|
||||||
|
secondary = area.left + primary_right + secondary_right;
|
||||||
|
stack = area.left;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
(primary, secondary, stack)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if len >= 1 {
|
||||||
|
layouts.push(Rect {
|
||||||
|
left: primary_left,
|
||||||
|
top: area.top,
|
||||||
|
right: primary_right,
|
||||||
|
bottom: area.bottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
if len >= 2 {
|
||||||
|
layouts.push(Rect {
|
||||||
|
left: secondary_left,
|
||||||
|
top: area.top,
|
||||||
|
right: secondary_right,
|
||||||
|
bottom: area.bottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
if len > 2 {
|
||||||
|
layouts.append(&mut rows(
|
||||||
|
&Rect {
|
||||||
|
left: stack_left,
|
||||||
|
top: area.top,
|
||||||
|
right: secondary_right,
|
||||||
|
bottom: area.bottom,
|
||||||
|
},
|
||||||
|
len - 2,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layouts
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dimensions
|
||||||
|
.iter_mut()
|
||||||
|
.for_each(|l| l.add_padding(container_padding));
|
||||||
|
|
||||||
|
dimensions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Arrangement for CustomLayout {
|
||||||
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
area: &Rect,
|
||||||
|
len: NonZeroUsize,
|
||||||
|
container_padding: Option<i32>,
|
||||||
|
_layout_flip: Option<Axis>,
|
||||||
|
_resize_dimensions: &[Option<Rect>],
|
||||||
|
) -> Vec<Rect> {
|
||||||
|
let mut dimensions = vec![];
|
||||||
|
let container_count = len.get();
|
||||||
|
|
||||||
|
if container_count <= self.len() {
|
||||||
|
let mut layouts = columns(area, container_count);
|
||||||
|
dimensions.append(&mut layouts);
|
||||||
|
} else {
|
||||||
|
let count_map = self.column_container_counts();
|
||||||
|
|
||||||
|
// If there are not enough windows to trigger the final tertiary
|
||||||
|
// column in the custom layout, use an offset to reduce the number of
|
||||||
|
// columns to calculate each column's area by, so that we don't have
|
||||||
|
// an empty ghost tertiary column and the screen space can be maximised
|
||||||
|
// until there are enough windows to create it
|
||||||
|
let mut tertiary_trigger_threshold = 0;
|
||||||
|
|
||||||
|
// always -1 because we don't insert the tertiary column in the count_map
|
||||||
|
for i in 0..self.len() - 1 {
|
||||||
|
tertiary_trigger_threshold += count_map.get(&i).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let enable_tertiary_column = len.get() > tertiary_trigger_threshold;
|
||||||
|
|
||||||
|
let offset = if enable_tertiary_column {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Option::from(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
||||||
|
let primary_right = self.primary_width_percentage().map_or_else(
|
||||||
|
|| area.right / self.len() as i32,
|
||||||
|
|percentage| (area.right / 100) * percentage as i32,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (idx, column) in self.iter().enumerate() {
|
||||||
|
// If we are offsetting a tertiary column for which the threshold
|
||||||
|
// has not yet been met, this loop should not run for that final
|
||||||
|
// tertiary column
|
||||||
|
if idx < self.len() - offset.unwrap_or(0) {
|
||||||
|
let column_area = if idx == 0 {
|
||||||
|
Self::column_area_with_last(self.len(), area, primary_right, None, offset)
|
||||||
|
} else {
|
||||||
|
Self::column_area_with_last(
|
||||||
|
self.len(),
|
||||||
|
area,
|
||||||
|
primary_right,
|
||||||
|
Option::from(dimensions[self.first_container_idx(idx - 1)]),
|
||||||
|
offset,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
match column {
|
||||||
|
Column::Primary(Option::Some(_)) => {
|
||||||
|
let main_column_area = if idx == 0 {
|
||||||
|
Self::main_column_area(area, primary_right, None)
|
||||||
|
} else {
|
||||||
|
Self::main_column_area(
|
||||||
|
area,
|
||||||
|
primary_right,
|
||||||
|
Option::from(dimensions[self.first_container_idx(idx - 1)]),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
dimensions.push(main_column_area);
|
||||||
|
}
|
||||||
|
Column::Primary(None) | Column::Secondary(None) => {
|
||||||
|
dimensions.push(column_area);
|
||||||
|
}
|
||||||
|
Column::Secondary(Some(split)) => match split {
|
||||||
|
ColumnSplitWithCapacity::Horizontal(capacity) => {
|
||||||
|
let mut rows = rows(&column_area, *capacity);
|
||||||
|
dimensions.append(&mut rows);
|
||||||
|
}
|
||||||
|
ColumnSplitWithCapacity::Vertical(capacity) => {
|
||||||
|
let mut columns = columns(&column_area, *capacity);
|
||||||
|
dimensions.append(&mut columns);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Column::Tertiary(split) => {
|
||||||
|
let column_area = Self::column_area_with_last(
|
||||||
|
self.len(),
|
||||||
|
area,
|
||||||
|
primary_right,
|
||||||
|
Option::from(dimensions[self.first_container_idx(idx - 1)]),
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
let remaining = container_count - tertiary_trigger_threshold;
|
||||||
|
|
||||||
|
match split {
|
||||||
|
ColumnSplit::Horizontal => {
|
||||||
|
let mut rows = rows(&column_area, remaining);
|
||||||
|
dimensions.append(&mut rows);
|
||||||
|
}
|
||||||
|
ColumnSplit::Vertical => {
|
||||||
|
let mut columns = columns(&column_area, remaining);
|
||||||
|
dimensions.append(&mut columns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dimensions
|
||||||
|
.iter_mut()
|
||||||
|
.for_each(|l| l.add_padding(container_padding));
|
||||||
|
|
||||||
|
dimensions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum Axis {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
HorizontalAndVertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn columns(area: &Rect, len: usize) -> Vec<Rect> {
|
||||||
|
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||||
|
let right = area.right / len as i32;
|
||||||
|
let mut left = 0;
|
||||||
|
|
||||||
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
|
for _ in 0..len {
|
||||||
|
layouts.push(Rect {
|
||||||
|
left: area.left + left,
|
||||||
|
top: area.top,
|
||||||
|
right,
|
||||||
|
bottom: area.bottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
left += right;
|
||||||
|
}
|
||||||
|
|
||||||
|
layouts
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn rows(area: &Rect, len: usize) -> Vec<Rect> {
|
||||||
|
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||||
|
let bottom = area.bottom / len as i32;
|
||||||
|
let mut top = 0;
|
||||||
|
|
||||||
|
let mut layouts: Vec<Rect> = vec![];
|
||||||
|
for _ in 0..len {
|
||||||
|
layouts.push(Rect {
|
||||||
|
left: area.left,
|
||||||
|
top: area.top + top,
|
||||||
|
right: area.right,
|
||||||
|
bottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
top += bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
layouts
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
|
||||||
|
let mut resize_adjustments = resize_dimensions.to_vec();
|
||||||
|
|
||||||
|
// This needs to be aware of layout flips
|
||||||
|
for (i, opt) in resize_dimensions.iter().enumerate() {
|
||||||
|
if let Some(resize_ref) = opt {
|
||||||
|
if i > 0 {
|
||||||
|
if resize_ref.left != 0 {
|
||||||
|
#[allow(clippy::if_not_else)]
|
||||||
|
let range = if i == 1 {
|
||||||
|
0..1
|
||||||
|
} else if i & 1 != 0 {
|
||||||
|
i - 1..i
|
||||||
|
} else {
|
||||||
|
i - 2..i
|
||||||
|
};
|
||||||
|
|
||||||
|
for n in range {
|
||||||
|
let should_adjust = n % 2 == 0;
|
||||||
|
if should_adjust {
|
||||||
|
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||||
|
adjacent_resize.right += resize_ref.left;
|
||||||
|
} else {
|
||||||
|
resize_adjustments[n] = Option::from(Rect {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: resize_ref.left,
|
||||||
|
bottom: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||||
|
rr.left = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resize_ref.top != 0 {
|
||||||
|
let range = if i == 1 {
|
||||||
|
0..1
|
||||||
|
} else if i & 1 == 0 {
|
||||||
|
i - 1..i
|
||||||
|
} else {
|
||||||
|
i - 2..i
|
||||||
|
};
|
||||||
|
|
||||||
|
for n in range {
|
||||||
|
let should_adjust = n % 2 != 0;
|
||||||
|
if should_adjust {
|
||||||
|
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||||
|
adjacent_resize.bottom += resize_ref.top;
|
||||||
|
} else {
|
||||||
|
resize_adjustments[n] = Option::from(Rect {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: resize_ref.top,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||||
|
resize.top = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleaned_resize_adjustments: Vec<_> = resize_adjustments
|
||||||
|
.iter()
|
||||||
|
.map(|adjustment| match adjustment {
|
||||||
|
None => None,
|
||||||
|
Some(rect) if rect.eq(&Rect::default()) => None,
|
||||||
|
Some(_) => *adjustment,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
cleaned_resize_adjustments
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::only_used_in_recursion)]
|
||||||
|
fn recursive_fibonacci(
|
||||||
|
idx: usize,
|
||||||
|
count: usize,
|
||||||
|
area: &Rect,
|
||||||
|
layout_flip: Option<Axis>,
|
||||||
|
resize_adjustments: Vec<Option<Rect>>,
|
||||||
|
) -> Vec<Rect> {
|
||||||
|
let mut a = *area;
|
||||||
|
|
||||||
|
let resized = if let Some(Some(r)) = resize_adjustments.get(idx) {
|
||||||
|
a.left += r.left;
|
||||||
|
a.top += r.top;
|
||||||
|
a.right += r.right;
|
||||||
|
a.bottom += r.bottom;
|
||||||
|
a
|
||||||
|
} else {
|
||||||
|
*area
|
||||||
|
};
|
||||||
|
|
||||||
|
let half_width = area.right / 2;
|
||||||
|
let half_height = area.bottom / 2;
|
||||||
|
let half_resized_width = resized.right / 2;
|
||||||
|
let half_resized_height = resized.bottom / 2;
|
||||||
|
|
||||||
|
let (main_x, alt_x, alt_y, main_y);
|
||||||
|
|
||||||
|
if let Some(flip) = layout_flip {
|
||||||
|
match flip {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
main_x = resized.left + half_width + (half_width - half_resized_width);
|
||||||
|
alt_x = resized.left;
|
||||||
|
|
||||||
|
alt_y = resized.top + half_resized_height;
|
||||||
|
main_y = resized.top;
|
||||||
|
}
|
||||||
|
Axis::Vertical => {
|
||||||
|
main_y = resized.top + half_height + (half_height - half_resized_height);
|
||||||
|
alt_y = resized.top;
|
||||||
|
|
||||||
|
main_x = resized.left;
|
||||||
|
alt_x = resized.left + half_resized_width;
|
||||||
|
}
|
||||||
|
Axis::HorizontalAndVertical => {
|
||||||
|
main_x = resized.left + half_width + (half_width - half_resized_width);
|
||||||
|
alt_x = resized.left;
|
||||||
|
main_y = resized.top + half_height + (half_height - half_resized_height);
|
||||||
|
alt_y = resized.top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
main_x = resized.left;
|
||||||
|
alt_x = resized.left + half_resized_width;
|
||||||
|
main_y = resized.top;
|
||||||
|
alt_y = resized.top + half_resized_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::if_not_else)]
|
||||||
|
if count == 0 {
|
||||||
|
vec![]
|
||||||
|
} else if count == 1 {
|
||||||
|
vec![Rect {
|
||||||
|
left: resized.left,
|
||||||
|
top: resized.top,
|
||||||
|
right: resized.right,
|
||||||
|
bottom: resized.bottom,
|
||||||
|
}]
|
||||||
|
} else if idx % 2 != 0 {
|
||||||
|
let mut res = vec![Rect {
|
||||||
|
left: resized.left,
|
||||||
|
top: main_y,
|
||||||
|
right: resized.right,
|
||||||
|
bottom: half_resized_height,
|
||||||
|
}];
|
||||||
|
res.append(&mut recursive_fibonacci(
|
||||||
|
idx + 1,
|
||||||
|
count - 1,
|
||||||
|
&Rect {
|
||||||
|
left: area.left,
|
||||||
|
top: alt_y,
|
||||||
|
right: area.right,
|
||||||
|
bottom: area.bottom - half_resized_height,
|
||||||
|
},
|
||||||
|
layout_flip,
|
||||||
|
resize_adjustments,
|
||||||
|
));
|
||||||
|
res
|
||||||
|
} else {
|
||||||
|
let mut res = vec![Rect {
|
||||||
|
left: main_x,
|
||||||
|
top: resized.top,
|
||||||
|
right: half_resized_width,
|
||||||
|
bottom: resized.bottom,
|
||||||
|
}];
|
||||||
|
res.append(&mut recursive_fibonacci(
|
||||||
|
idx + 1,
|
||||||
|
count - 1,
|
||||||
|
&Rect {
|
||||||
|
left: alt_x,
|
||||||
|
top: area.top,
|
||||||
|
right: area.right - half_resized_width,
|
||||||
|
bottom: area.bottom,
|
||||||
|
},
|
||||||
|
layout_flip,
|
||||||
|
resize_adjustments,
|
||||||
|
));
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
176
komorebi-core/src/config_generation.rs
Normal file
176
komorebi-core/src/config_generation.rs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
use clap::ArgEnum;
|
||||||
|
use color_eyre::Result;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use strum::Display;
|
||||||
|
use strum::EnumString;
|
||||||
|
|
||||||
|
use crate::ApplicationIdentifier;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ApplicationOptions {
|
||||||
|
ObjectNameChange,
|
||||||
|
Layered,
|
||||||
|
BorderOverflow,
|
||||||
|
TrayAndMultiWindow,
|
||||||
|
Force,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationOptions {
|
||||||
|
#[must_use]
|
||||||
|
pub fn cfgen(&self, kind: &ApplicationIdentifier, id: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"Run, {}, , Hide",
|
||||||
|
match self {
|
||||||
|
ApplicationOptions::ObjectNameChange => {
|
||||||
|
format!(
|
||||||
|
"komorebic.exe identify-object-name-change-application {} \"{}\"",
|
||||||
|
kind, id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ApplicationOptions::Layered => {
|
||||||
|
format!(
|
||||||
|
"komorebic.exe identify-layered-application {} \"{}\"",
|
||||||
|
kind, id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ApplicationOptions::BorderOverflow => {
|
||||||
|
format!(
|
||||||
|
"komorebic.exe identify-border-overflow-application {} \"{}\"",
|
||||||
|
kind, id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ApplicationOptions::TrayAndMultiWindow => {
|
||||||
|
format!(
|
||||||
|
"komorebic.exe identify-tray-application {} \"{}\"",
|
||||||
|
kind, id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ApplicationOptions::Force => {
|
||||||
|
format!("komorebic.exe manage-rule {} \"{}\"", kind, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct IdWithIdentifier {
|
||||||
|
kind: ApplicationIdentifier,
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct IdWithIdentifierAndComment {
|
||||||
|
kind: ApplicationIdentifier,
|
||||||
|
id: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
comment: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct ApplicationConfiguration {
|
||||||
|
name: String,
|
||||||
|
identifier: IdWithIdentifier,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
options: Option<Vec<ApplicationOptions>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
float_identifiers: Option<Vec<IdWithIdentifierAndComment>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct ApplicationConfigurationGenerator;
|
||||||
|
|
||||||
|
impl ApplicationConfigurationGenerator {
|
||||||
|
fn load(content: &str) -> Result<Vec<ApplicationConfiguration>> {
|
||||||
|
Ok(serde_yaml::from_str(content)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(content: &str) -> Result<String> {
|
||||||
|
let mut cfgen = Self::load(content)?;
|
||||||
|
cfgen.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
Ok(serde_yaml::to_string(&cfgen)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge(base_content: &str, override_content: &str) -> Result<Vec<ApplicationConfiguration>> {
|
||||||
|
let base_cfgen = Self::load(base_content)?;
|
||||||
|
let override_cfgen = Self::load(override_content)?;
|
||||||
|
|
||||||
|
let mut final_cfgen = base_cfgen.clone();
|
||||||
|
|
||||||
|
for entry in override_cfgen {
|
||||||
|
let mut replace_idx = None;
|
||||||
|
for (idx, base_entry) in base_cfgen.iter().enumerate() {
|
||||||
|
if base_entry.name == entry.name {
|
||||||
|
replace_idx = Option::from(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match replace_idx {
|
||||||
|
None => final_cfgen.push(entry),
|
||||||
|
Some(idx) => final_cfgen[idx] = entry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(final_cfgen)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_ahk(base_content: &str, override_content: Option<&str>) -> Result<Vec<String>> {
|
||||||
|
let mut cfgen = if let Some(override_content) = override_content {
|
||||||
|
Self::merge(base_content, override_content)?
|
||||||
|
} else {
|
||||||
|
Self::load(base_content)?
|
||||||
|
};
|
||||||
|
|
||||||
|
cfgen.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
||||||
|
let mut lines = vec![
|
||||||
|
String::from("; Generated by komorebic.exe"),
|
||||||
|
String::from("; To use this file, add the line below to the top of your komorebi.ahk configuration file"),
|
||||||
|
String::from("; #Include %A_ScriptDir%\\komorebi.generated.ahk"),
|
||||||
|
String::from("")
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut float_rules = vec![];
|
||||||
|
|
||||||
|
for app in cfgen {
|
||||||
|
lines.push(format!("; {}", app.name));
|
||||||
|
if let Some(options) = app.options {
|
||||||
|
for opt in options {
|
||||||
|
if let ApplicationOptions::TrayAndMultiWindow = opt {
|
||||||
|
lines.push(String::from("; If you have disabled minimize/close to tray for this application, you can delete/comment out the next line"));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(opt.cfgen(&app.identifier.kind, &app.identifier.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(float_identifiers) = app.float_identifiers {
|
||||||
|
for float in float_identifiers {
|
||||||
|
let float_rule = format!(
|
||||||
|
"Run, komorebic.exe float-rule {} \"{}\", , Hide",
|
||||||
|
float.kind, float.id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't want to send duped signals especially as configs get larger
|
||||||
|
if !float_rules.contains(&float_rule) {
|
||||||
|
float_rules.push(float_rule.clone());
|
||||||
|
|
||||||
|
if let Some(comment) = float.comment {
|
||||||
|
lines.push(format!("; {}", comment));
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.push(float_rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(String::from(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
278
komorebi-core/src/custom_layout.rs
Normal file
278
komorebi-core/src/custom_layout.rs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::ops::DerefMut;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use color_eyre::eyre::anyhow;
|
||||||
|
use color_eyre::Result;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::Rect;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct CustomLayout(Vec<Column>);
|
||||||
|
|
||||||
|
impl Deref for CustomLayout {
|
||||||
|
type Target = Vec<Column>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for CustomLayout {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomLayout {
|
||||||
|
pub fn from_path_buf(path: PathBuf) -> Result<Self> {
|
||||||
|
let invalid_filetype = anyhow!("custom layouts must be json or yaml files");
|
||||||
|
let layout: Self = match path.extension() {
|
||||||
|
Some(extension) => {
|
||||||
|
if extension == "yaml" || extension == "yml" {
|
||||||
|
serde_yaml::from_reader(BufReader::new(File::open(path)?))?
|
||||||
|
} else if extension == "json" {
|
||||||
|
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
||||||
|
} else {
|
||||||
|
return Err(invalid_filetype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return Err(invalid_filetype),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !layout.is_valid() {
|
||||||
|
return Err(anyhow!("the layout file provided was invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn column_with_idx(&self, idx: usize) -> (usize, Option<&Column>) {
|
||||||
|
let column_idx = self.column_for_container_idx(idx);
|
||||||
|
let column = self.get(column_idx);
|
||||||
|
(column_idx, column)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn primary_idx(&self) -> Option<usize> {
|
||||||
|
for (i, column) in self.iter().enumerate() {
|
||||||
|
if let Column::Primary(_) = column {
|
||||||
|
return Option::from(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn primary_width_percentage(&self) -> Option<usize> {
|
||||||
|
for column in self.iter() {
|
||||||
|
if let Column::Primary(Option::Some(ColumnWidth::WidthPercentage(percentage))) = column
|
||||||
|
{
|
||||||
|
return Option::from(*percentage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_primary_width_percentage(&mut self, percentage: usize) {
|
||||||
|
for column in self.iter_mut() {
|
||||||
|
if let Column::Primary(Option::Some(ColumnWidth::WidthPercentage(current))) = column {
|
||||||
|
*current = percentage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
// A valid layout must have at least one column
|
||||||
|
if self.is_empty() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vertical column splits aren't supported at the moment
|
||||||
|
for column in self.iter() {
|
||||||
|
match column {
|
||||||
|
Column::Tertiary(ColumnSplit::Vertical)
|
||||||
|
| Column::Secondary(Some(ColumnSplitWithCapacity::Vertical(_))) => return false,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The final column must not have a fixed capacity
|
||||||
|
match self.last() {
|
||||||
|
Some(Column::Tertiary(_)) => {}
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut primaries = 0;
|
||||||
|
let mut tertiaries = 0;
|
||||||
|
|
||||||
|
for column in self.iter() {
|
||||||
|
match column {
|
||||||
|
Column::Primary(_) => primaries += 1,
|
||||||
|
Column::Tertiary(_) => tertiaries += 1,
|
||||||
|
Column::Secondary(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There must only be one primary and one tertiary column
|
||||||
|
matches!(primaries, 1) && matches!(tertiaries, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn column_container_counts(&self) -> HashMap<usize, usize> {
|
||||||
|
let mut count_map = HashMap::new();
|
||||||
|
|
||||||
|
for (idx, column) in self.iter().enumerate() {
|
||||||
|
match column {
|
||||||
|
Column::Primary(_) | Column::Secondary(None) => {
|
||||||
|
count_map.insert(idx, 1);
|
||||||
|
}
|
||||||
|
Column::Secondary(Some(split)) => {
|
||||||
|
count_map.insert(
|
||||||
|
idx,
|
||||||
|
match split {
|
||||||
|
ColumnSplitWithCapacity::Vertical(n)
|
||||||
|
| ColumnSplitWithCapacity::Horizontal(n) => *n,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Column::Tertiary(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count_map
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn first_container_idx(&self, col_idx: usize) -> usize {
|
||||||
|
let count_map = self.column_container_counts();
|
||||||
|
let mut container_idx_accumulator = 0;
|
||||||
|
|
||||||
|
for i in 0..col_idx {
|
||||||
|
if let Some(n) = count_map.get(&i) {
|
||||||
|
container_idx_accumulator += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container_idx_accumulator
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn column_for_container_idx(&self, idx: usize) -> usize {
|
||||||
|
let count_map = self.column_container_counts();
|
||||||
|
let mut container_idx_accumulator = 0;
|
||||||
|
|
||||||
|
// always -1 because we don't insert the tertiary column in the count_map
|
||||||
|
for i in 0..self.len() - 1 {
|
||||||
|
if let Some(n) = count_map.get(&i) {
|
||||||
|
container_idx_accumulator += n;
|
||||||
|
|
||||||
|
// The accumulator becomes greater than the window container index
|
||||||
|
// for the first time when we reach a column that contains that
|
||||||
|
// window container index
|
||||||
|
if container_idx_accumulator > idx {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the accumulator never reaches a point where it is greater than the
|
||||||
|
// window container index, then the only remaining possibility is the
|
||||||
|
// final tertiary column
|
||||||
|
self.len() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn column_area(&self, work_area: &Rect, idx: usize, offset: Option<usize>) -> Rect {
|
||||||
|
let divisor = offset.map_or_else(|| self.len(), |offset| self.len() - offset);
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||||
|
let equal_width = work_area.right / divisor as i32;
|
||||||
|
let mut left = work_area.left;
|
||||||
|
let right = equal_width;
|
||||||
|
|
||||||
|
for _ in 0..idx {
|
||||||
|
left += right;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect {
|
||||||
|
left,
|
||||||
|
top: work_area.top,
|
||||||
|
right,
|
||||||
|
bottom: work_area.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn column_area_with_last(
|
||||||
|
len: usize,
|
||||||
|
work_area: &Rect,
|
||||||
|
primary_right: i32,
|
||||||
|
last_column: Option<Rect>,
|
||||||
|
offset: Option<usize>,
|
||||||
|
) -> Rect {
|
||||||
|
let divisor = offset.map_or_else(|| len - 1, |offset| len - offset - 1);
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
|
||||||
|
let equal_width = (work_area.right - primary_right) / divisor as i32;
|
||||||
|
let left = last_column.map_or(work_area.left, |last| last.left + last.right);
|
||||||
|
let right = equal_width;
|
||||||
|
|
||||||
|
Rect {
|
||||||
|
left,
|
||||||
|
top: work_area.top,
|
||||||
|
right,
|
||||||
|
bottom: work_area.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn main_column_area(
|
||||||
|
work_area: &Rect,
|
||||||
|
primary_right: i32,
|
||||||
|
last_column: Option<Rect>,
|
||||||
|
) -> Rect {
|
||||||
|
let left = last_column.map_or(work_area.left, |last| last.left + last.right);
|
||||||
|
|
||||||
|
Rect {
|
||||||
|
left,
|
||||||
|
top: work_area.top,
|
||||||
|
right: primary_right,
|
||||||
|
bottom: work_area.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
#[serde(tag = "column", content = "configuration")]
|
||||||
|
pub enum Column {
|
||||||
|
Primary(Option<ColumnWidth>),
|
||||||
|
Secondary(Option<ColumnSplitWithCapacity>),
|
||||||
|
Tertiary(ColumnSplit),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub enum ColumnWidth {
|
||||||
|
WidthPercentage(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub enum ColumnSplit {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub enum ColumnSplitWithCapacity {
|
||||||
|
Horizontal(usize),
|
||||||
|
Vertical(usize),
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use clap::ArgEnum;
|
use clap::ArgEnum;
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum CycleDirection {
|
pub enum CycleDirection {
|
||||||
Previous,
|
Previous,
|
||||||
@@ -13,17 +16,17 @@ pub enum CycleDirection {
|
|||||||
|
|
||||||
impl CycleDirection {
|
impl CycleDirection {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn next_idx(&self, idx: usize, len: usize) -> usize {
|
pub const fn next_idx(&self, idx: usize, len: NonZeroUsize) -> usize {
|
||||||
match self {
|
match self {
|
||||||
CycleDirection::Previous => {
|
Self::Previous => {
|
||||||
if idx == 0 {
|
if idx == 0 {
|
||||||
len - 1
|
len.get() - 1
|
||||||
} else {
|
} else {
|
||||||
idx - 1
|
idx - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CycleDirection::Next => {
|
Self::Next => {
|
||||||
if idx == len - 1 {
|
if idx == len.get() - 1 {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
idx + 1
|
idx + 1
|
||||||
|
|||||||
126
komorebi-core/src/default_layout.rs
Normal file
126
komorebi-core/src/default_layout.rs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
use clap::ArgEnum;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use strum::Display;
|
||||||
|
use strum::EnumString;
|
||||||
|
|
||||||
|
use crate::OperationDirection;
|
||||||
|
use crate::Rect;
|
||||||
|
use crate::Sizing;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum DefaultLayout {
|
||||||
|
BSP,
|
||||||
|
Columns,
|
||||||
|
Rows,
|
||||||
|
VerticalStack,
|
||||||
|
HorizontalStack,
|
||||||
|
UltrawideVerticalStack,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultLayout {
|
||||||
|
#[must_use]
|
||||||
|
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
|
||||||
|
pub fn resize(
|
||||||
|
&self,
|
||||||
|
unaltered: &Rect,
|
||||||
|
resize: &Option<Rect>,
|
||||||
|
edge: OperationDirection,
|
||||||
|
sizing: Sizing,
|
||||||
|
delta: i32,
|
||||||
|
) -> Option<Rect> {
|
||||||
|
if !matches!(self, Self::BSP) {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_divisor = 1.005;
|
||||||
|
let mut r = resize.unwrap_or_default();
|
||||||
|
|
||||||
|
let resize_delta = delta;
|
||||||
|
|
||||||
|
match edge {
|
||||||
|
OperationDirection::Left => match sizing {
|
||||||
|
Sizing::Increase => {
|
||||||
|
// Some final checks to make sure the user can't infinitely resize to
|
||||||
|
// the point of pushing other windows out of bounds
|
||||||
|
|
||||||
|
// Note: These checks cannot take into account the changes made to the
|
||||||
|
// edges of adjacent windows at operation time, so it is still possible
|
||||||
|
// to push windows out of bounds by maxing out an Increase Left on a
|
||||||
|
// Window with index 1, and then maxing out a Decrease Right on a Window
|
||||||
|
// with index 0. I don't think it's worth trying to defensively program
|
||||||
|
// against this; if people end up in this situation they are better off
|
||||||
|
// just hitting the retile command
|
||||||
|
let diff = ((r.left + -resize_delta) as f32).abs();
|
||||||
|
let max = unaltered.right as f32 / max_divisor;
|
||||||
|
if diff < max {
|
||||||
|
r.left += -resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sizing::Decrease => {
|
||||||
|
let diff = ((r.left - -resize_delta) as f32).abs();
|
||||||
|
let max = unaltered.right as f32 / max_divisor;
|
||||||
|
if diff < max {
|
||||||
|
r.left -= -resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OperationDirection::Up => match sizing {
|
||||||
|
Sizing::Increase => {
|
||||||
|
let diff = ((r.top + resize_delta) as f32).abs();
|
||||||
|
let max = unaltered.bottom as f32 / max_divisor;
|
||||||
|
if diff < max {
|
||||||
|
r.top += -resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sizing::Decrease => {
|
||||||
|
let diff = ((r.top - resize_delta) as f32).abs();
|
||||||
|
let max = unaltered.bottom as f32 / max_divisor;
|
||||||
|
if diff < max {
|
||||||
|
r.top -= -resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OperationDirection::Right => match sizing {
|
||||||
|
Sizing::Increase => {
|
||||||
|
let diff = ((r.right + resize_delta) as f32).abs();
|
||||||
|
let max = unaltered.right as f32 / max_divisor;
|
||||||
|
if diff < max {
|
||||||
|
r.right += resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sizing::Decrease => {
|
||||||
|
let diff = ((r.right - resize_delta) as f32).abs();
|
||||||
|
let max = unaltered.right as f32 / max_divisor;
|
||||||
|
if diff < max {
|
||||||
|
r.right -= resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OperationDirection::Down => match sizing {
|
||||||
|
Sizing::Increase => {
|
||||||
|
let diff = ((r.bottom + resize_delta) as f32).abs();
|
||||||
|
let max = unaltered.bottom as f32 / max_divisor;
|
||||||
|
if diff < max {
|
||||||
|
r.bottom += resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sizing::Decrease => {
|
||||||
|
let diff = ((r.bottom - resize_delta) as f32).abs();
|
||||||
|
let max = unaltered.bottom as f32 / max_divisor;
|
||||||
|
if diff < max {
|
||||||
|
r.bottom -= resize_delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if r.eq(&Rect::default()) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Option::from(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
284
komorebi-core/src/direction.rs
Normal file
284
komorebi-core/src/direction.rs
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
use crate::custom_layout::Column;
|
||||||
|
use crate::custom_layout::ColumnSplit;
|
||||||
|
use crate::custom_layout::ColumnSplitWithCapacity;
|
||||||
|
use crate::custom_layout::CustomLayout;
|
||||||
|
use crate::DefaultLayout;
|
||||||
|
use crate::OperationDirection;
|
||||||
|
|
||||||
|
pub trait Direction {
|
||||||
|
fn index_in_direction(
|
||||||
|
&self,
|
||||||
|
op_direction: OperationDirection,
|
||||||
|
idx: usize,
|
||||||
|
count: usize,
|
||||||
|
) -> Option<usize>;
|
||||||
|
|
||||||
|
fn is_valid_direction(
|
||||||
|
&self,
|
||||||
|
op_direction: OperationDirection,
|
||||||
|
idx: usize,
|
||||||
|
count: usize,
|
||||||
|
) -> bool;
|
||||||
|
fn up_index(&self, idx: usize) -> usize;
|
||||||
|
fn down_index(&self, idx: usize) -> usize;
|
||||||
|
fn left_index(&self, idx: usize) -> usize;
|
||||||
|
fn right_index(&self, idx: usize) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Direction for DefaultLayout {
|
||||||
|
fn index_in_direction(
|
||||||
|
&self,
|
||||||
|
op_direction: OperationDirection,
|
||||||
|
idx: usize,
|
||||||
|
count: usize,
|
||||||
|
) -> Option<usize> {
|
||||||
|
match op_direction {
|
||||||
|
OperationDirection::Left => {
|
||||||
|
if self.is_valid_direction(op_direction, idx, count) {
|
||||||
|
Option::from(self.left_index(idx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OperationDirection::Right => {
|
||||||
|
if self.is_valid_direction(op_direction, idx, count) {
|
||||||
|
Option::from(self.right_index(idx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OperationDirection::Up => {
|
||||||
|
if self.is_valid_direction(op_direction, idx, count) {
|
||||||
|
Option::from(self.up_index(idx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OperationDirection::Down => {
|
||||||
|
if self.is_valid_direction(op_direction, idx, count) {
|
||||||
|
Option::from(self.down_index(idx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_direction(
|
||||||
|
&self,
|
||||||
|
op_direction: OperationDirection,
|
||||||
|
idx: usize,
|
||||||
|
count: usize,
|
||||||
|
) -> bool {
|
||||||
|
match op_direction {
|
||||||
|
OperationDirection::Up => match self {
|
||||||
|
Self::BSP => count > 2 && idx != 0 && idx != 1,
|
||||||
|
Self::Columns => false,
|
||||||
|
Self::Rows | Self::HorizontalStack => idx != 0,
|
||||||
|
Self::VerticalStack => idx != 0 && idx != 1,
|
||||||
|
Self::UltrawideVerticalStack => idx > 2,
|
||||||
|
},
|
||||||
|
OperationDirection::Down => match self {
|
||||||
|
Self::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
|
||||||
|
Self::Columns => false,
|
||||||
|
Self::Rows => idx != count - 1,
|
||||||
|
Self::VerticalStack => idx != 0 && idx != count - 1,
|
||||||
|
Self::HorizontalStack => idx == 0,
|
||||||
|
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||||
|
},
|
||||||
|
OperationDirection::Left => match self {
|
||||||
|
Self::BSP => count > 1 && idx != 0,
|
||||||
|
Self::Columns | Self::VerticalStack => idx != 0,
|
||||||
|
Self::Rows => false,
|
||||||
|
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||||
|
Self::UltrawideVerticalStack => count > 1 && idx != 1,
|
||||||
|
},
|
||||||
|
OperationDirection::Right => match self {
|
||||||
|
Self::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
|
||||||
|
Self::Columns => idx != count - 1,
|
||||||
|
Self::Rows => false,
|
||||||
|
Self::VerticalStack => idx == 0,
|
||||||
|
Self::HorizontalStack => idx != 0 && idx != count - 1,
|
||||||
|
Self::UltrawideVerticalStack => match count {
|
||||||
|
0 | 1 => false,
|
||||||
|
2 => idx != 0,
|
||||||
|
_ => idx < 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn up_index(&self, idx: usize) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::BSP => {
|
||||||
|
if idx % 2 == 0 {
|
||||||
|
idx - 1
|
||||||
|
} else {
|
||||||
|
idx - 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Columns => unreachable!(),
|
||||||
|
Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx - 1,
|
||||||
|
Self::HorizontalStack => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down_index(&self, idx: usize) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::BSP | Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx + 1,
|
||||||
|
Self::Columns => unreachable!(),
|
||||||
|
Self::HorizontalStack => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn left_index(&self, idx: usize) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::BSP => {
|
||||||
|
if idx % 2 == 0 {
|
||||||
|
idx - 2
|
||||||
|
} else {
|
||||||
|
idx - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Columns | Self::HorizontalStack => idx - 1,
|
||||||
|
Self::Rows => unreachable!(),
|
||||||
|
Self::VerticalStack => 0,
|
||||||
|
Self::UltrawideVerticalStack => match idx {
|
||||||
|
0 => 1,
|
||||||
|
1 => unreachable!(),
|
||||||
|
_ => 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn right_index(&self, idx: usize) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
|
||||||
|
Self::Rows => unreachable!(),
|
||||||
|
Self::VerticalStack => 1,
|
||||||
|
Self::UltrawideVerticalStack => match idx {
|
||||||
|
1 => 0,
|
||||||
|
0 => 2,
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Direction for CustomLayout {
|
||||||
|
fn index_in_direction(
|
||||||
|
&self,
|
||||||
|
op_direction: OperationDirection,
|
||||||
|
idx: usize,
|
||||||
|
count: usize,
|
||||||
|
) -> Option<usize> {
|
||||||
|
if count <= self.len() {
|
||||||
|
return DefaultLayout::Columns.index_in_direction(op_direction, idx, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
match op_direction {
|
||||||
|
OperationDirection::Left => {
|
||||||
|
if self.is_valid_direction(op_direction, idx, count) {
|
||||||
|
Option::from(self.left_index(idx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OperationDirection::Right => {
|
||||||
|
if self.is_valid_direction(op_direction, idx, count) {
|
||||||
|
Option::from(self.right_index(idx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OperationDirection::Up => {
|
||||||
|
if self.is_valid_direction(op_direction, idx, count) {
|
||||||
|
Option::from(self.up_index(idx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OperationDirection::Down => {
|
||||||
|
if self.is_valid_direction(op_direction, idx, count) {
|
||||||
|
Option::from(self.down_index(idx))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_direction(
|
||||||
|
&self,
|
||||||
|
op_direction: OperationDirection,
|
||||||
|
idx: usize,
|
||||||
|
count: usize,
|
||||||
|
) -> bool {
|
||||||
|
if count <= self.len() {
|
||||||
|
return DefaultLayout::Columns.is_valid_direction(op_direction, idx, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
match op_direction {
|
||||||
|
OperationDirection::Left => idx != 0 && self.column_for_container_idx(idx) != 0,
|
||||||
|
OperationDirection::Right => {
|
||||||
|
idx != count - 1 && self.column_for_container_idx(idx) != self.len() - 1
|
||||||
|
}
|
||||||
|
OperationDirection::Up => {
|
||||||
|
if idx == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (column_idx, column) = self.column_with_idx(idx);
|
||||||
|
match column {
|
||||||
|
None => false,
|
||||||
|
Some(column) => match column {
|
||||||
|
Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_)))
|
||||||
|
| Column::Tertiary(ColumnSplit::Horizontal) => {
|
||||||
|
self.column_for_container_idx(idx - 1) == column_idx
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OperationDirection::Down => {
|
||||||
|
if idx == count - 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (column_idx, column) = self.column_with_idx(idx);
|
||||||
|
match column {
|
||||||
|
None => false,
|
||||||
|
Some(column) => match column {
|
||||||
|
Column::Secondary(Some(ColumnSplitWithCapacity::Horizontal(_)))
|
||||||
|
| Column::Tertiary(ColumnSplit::Horizontal) => {
|
||||||
|
self.column_for_container_idx(idx + 1) == column_idx
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn up_index(&self, idx: usize) -> usize {
|
||||||
|
idx - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down_index(&self, idx: usize) -> usize {
|
||||||
|
idx + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn left_index(&self, idx: usize) -> usize {
|
||||||
|
let column_idx = self.column_for_container_idx(idx);
|
||||||
|
if column_idx - 1 == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.first_container_idx(column_idx - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn right_index(&self, idx: usize) -> usize {
|
||||||
|
let column_idx = self.column_for_container_idx(idx);
|
||||||
|
self.first_container_idx(column_idx + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,388 +1,32 @@
|
|||||||
use std::num::NonZeroUsize;
|
use schemars::JsonSchema;
|
||||||
|
|
||||||
use clap::ArgEnum;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use strum::Display;
|
|
||||||
use strum::EnumString;
|
|
||||||
|
|
||||||
use crate::OperationDirection;
|
use crate::Arrangement;
|
||||||
use crate::Rect;
|
use crate::CustomLayout;
|
||||||
use crate::Sizing;
|
use crate::DefaultLayout;
|
||||||
|
use crate::Direction;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
|
||||||
pub enum Layout {
|
pub enum Layout {
|
||||||
BSP,
|
Default(DefaultLayout),
|
||||||
Columns,
|
Custom(CustomLayout),
|
||||||
Rows,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
|
||||||
#[strum(serialize_all = "snake_case")]
|
|
||||||
pub enum Flip {
|
|
||||||
Horizontal,
|
|
||||||
Vertical,
|
|
||||||
HorizontalAndVertical,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout {
|
impl Layout {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(clippy::cast_precision_loss)]
|
pub fn as_boxed_direction(&self) -> Box<dyn Direction> {
|
||||||
pub fn resize(
|
match self {
|
||||||
&self,
|
Layout::Default(layout) => Box::new(*layout),
|
||||||
unaltered: &Rect,
|
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||||
resize: &Option<Rect>,
|
|
||||||
edge: OperationDirection,
|
|
||||||
sizing: Sizing,
|
|
||||||
step: Option<i32>,
|
|
||||||
) -> Option<Rect> {
|
|
||||||
if !matches!(self, Self::BSP) {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let max_divisor = 1.005;
|
|
||||||
let mut r = resize.unwrap_or_default();
|
|
||||||
|
|
||||||
let resize_step = step.unwrap_or(50);
|
|
||||||
|
|
||||||
match edge {
|
|
||||||
OperationDirection::Left => match sizing {
|
|
||||||
Sizing::Increase => {
|
|
||||||
// Some final checks to make sure the user can't infinitely resize to
|
|
||||||
// the point of pushing other windows out of bounds
|
|
||||||
|
|
||||||
// Note: These checks cannot take into account the changes made to the
|
|
||||||
// edges of adjacent windows at operation time, so it is still possible
|
|
||||||
// to push windows out of bounds by maxing out an Increase Left on a
|
|
||||||
// Window with index 1, and then maxing out a Decrease Right on a Window
|
|
||||||
// with index 0. I don't think it's worth trying to defensively program
|
|
||||||
// against this; if people end up in this situation they are better off
|
|
||||||
// just hitting the retile command
|
|
||||||
let diff = ((r.left + -resize_step) as f32).abs();
|
|
||||||
let max = unaltered.right as f32 / max_divisor;
|
|
||||||
if diff < max {
|
|
||||||
r.left += -resize_step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sizing::Decrease => {
|
|
||||||
let diff = ((r.left - -resize_step) as f32).abs();
|
|
||||||
let max = unaltered.right as f32 / max_divisor;
|
|
||||||
if diff < max {
|
|
||||||
r.left -= -resize_step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
OperationDirection::Up => match sizing {
|
|
||||||
Sizing::Increase => {
|
|
||||||
let diff = ((r.top + resize_step) as f32).abs();
|
|
||||||
let max = unaltered.bottom as f32 / max_divisor;
|
|
||||||
if diff < max {
|
|
||||||
r.top += -resize_step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sizing::Decrease => {
|
|
||||||
let diff = ((r.top - resize_step) as f32).abs();
|
|
||||||
let max = unaltered.bottom as f32 / max_divisor;
|
|
||||||
if diff < max {
|
|
||||||
r.top -= -resize_step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
OperationDirection::Right => match sizing {
|
|
||||||
Sizing::Increase => {
|
|
||||||
let diff = ((r.right + resize_step) as f32).abs();
|
|
||||||
let max = unaltered.right as f32 / max_divisor;
|
|
||||||
if diff < max {
|
|
||||||
r.right += resize_step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sizing::Decrease => {
|
|
||||||
let diff = ((r.right - resize_step) as f32).abs();
|
|
||||||
let max = unaltered.right as f32 / max_divisor;
|
|
||||||
if diff < max {
|
|
||||||
r.right -= resize_step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
OperationDirection::Down => match sizing {
|
|
||||||
Sizing::Increase => {
|
|
||||||
let diff = ((r.bottom + resize_step) as f32).abs();
|
|
||||||
let max = unaltered.bottom as f32 / max_divisor;
|
|
||||||
if diff < max {
|
|
||||||
r.bottom += resize_step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sizing::Decrease => {
|
|
||||||
let diff = ((r.bottom - resize_step) as f32).abs();
|
|
||||||
let max = unaltered.bottom as f32 / max_divisor;
|
|
||||||
if diff < max {
|
|
||||||
r.bottom -= resize_step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if r.eq(&Rect::default()) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Option::from(r)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
pub fn as_boxed_arrangement(&self) -> Box<dyn Arrangement> {
|
||||||
pub fn calculate(
|
match self {
|
||||||
&self,
|
Layout::Default(layout) => Box::new(*layout),
|
||||||
area: &Rect,
|
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||||
len: NonZeroUsize,
|
|
||||||
container_padding: Option<i32>,
|
|
||||||
layout_flip: Option<Flip>,
|
|
||||||
resize_dimensions: &[Option<Rect>],
|
|
||||||
) -> Vec<Rect> {
|
|
||||||
let len = usize::from(len);
|
|
||||||
let mut dimensions = match self {
|
|
||||||
Layout::BSP => recursive_fibonacci(
|
|
||||||
0,
|
|
||||||
len,
|
|
||||||
area,
|
|
||||||
layout_flip,
|
|
||||||
calculate_resize_adjustments(resize_dimensions),
|
|
||||||
),
|
|
||||||
Layout::Columns => {
|
|
||||||
let right = area.right / len as i32;
|
|
||||||
let mut left = 0;
|
|
||||||
|
|
||||||
let mut layouts: Vec<Rect> = vec![];
|
|
||||||
for _ in 0..len {
|
|
||||||
layouts.push(Rect {
|
|
||||||
left: area.left + left,
|
|
||||||
top: area.top,
|
|
||||||
right,
|
|
||||||
bottom: area.bottom,
|
|
||||||
});
|
|
||||||
|
|
||||||
left += right;
|
|
||||||
}
|
|
||||||
|
|
||||||
layouts
|
|
||||||
}
|
|
||||||
Layout::Rows => {
|
|
||||||
let bottom = area.bottom / len as i32;
|
|
||||||
let mut top = 0;
|
|
||||||
|
|
||||||
let mut layouts: Vec<Rect> = vec![];
|
|
||||||
for _ in 0..len {
|
|
||||||
layouts.push(Rect {
|
|
||||||
left: area.left,
|
|
||||||
top: area.top + top,
|
|
||||||
right: area.right,
|
|
||||||
bottom,
|
|
||||||
});
|
|
||||||
|
|
||||||
top += bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
layouts
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
dimensions
|
|
||||||
.iter_mut()
|
|
||||||
.for_each(|l| l.add_padding(container_padding));
|
|
||||||
|
|
||||||
dimensions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
|
|
||||||
let mut resize_adjustments = resize_dimensions.to_vec();
|
|
||||||
|
|
||||||
// This needs to be aware of layout flips
|
|
||||||
for (i, opt) in resize_dimensions.iter().enumerate() {
|
|
||||||
if let Some(resize_ref) = opt {
|
|
||||||
if i > 0 {
|
|
||||||
if resize_ref.left != 0 {
|
|
||||||
#[allow(clippy::if_not_else)]
|
|
||||||
let range = if i == 1 {
|
|
||||||
0..1
|
|
||||||
} else if i & 1 != 0 {
|
|
||||||
i - 1..i
|
|
||||||
} else {
|
|
||||||
i - 2..i
|
|
||||||
};
|
|
||||||
|
|
||||||
for n in range {
|
|
||||||
let should_adjust = n % 2 == 0;
|
|
||||||
if should_adjust {
|
|
||||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
|
||||||
adjacent_resize.right += resize_ref.left;
|
|
||||||
} else {
|
|
||||||
resize_adjustments[n] = Option::from(Rect {
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
right: resize_ref.left,
|
|
||||||
bottom: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
|
||||||
rr.left = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if resize_ref.top != 0 {
|
|
||||||
let range = if i == 1 {
|
|
||||||
0..1
|
|
||||||
} else if i & 1 == 0 {
|
|
||||||
i - 1..i
|
|
||||||
} else {
|
|
||||||
i - 2..i
|
|
||||||
};
|
|
||||||
|
|
||||||
for n in range {
|
|
||||||
let should_adjust = n % 2 != 0;
|
|
||||||
if should_adjust {
|
|
||||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
|
||||||
adjacent_resize.bottom += resize_ref.top;
|
|
||||||
} else {
|
|
||||||
resize_adjustments[n] = Option::from(Rect {
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: resize_ref.top,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
|
||||||
resize.top = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cleaned_resize_adjustments: Vec<_> = resize_adjustments
|
|
||||||
.iter()
|
|
||||||
.map(|adjustment| match adjustment {
|
|
||||||
None => None,
|
|
||||||
Some(rect) if rect.eq(&Rect::default()) => None,
|
|
||||||
Some(_) => *adjustment,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
cleaned_resize_adjustments
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recursive_fibonacci(
|
|
||||||
idx: usize,
|
|
||||||
count: usize,
|
|
||||||
area: &Rect,
|
|
||||||
layout_flip: Option<Flip>,
|
|
||||||
resize_adjustments: Vec<Option<Rect>>,
|
|
||||||
) -> Vec<Rect> {
|
|
||||||
let mut a = *area;
|
|
||||||
|
|
||||||
let resized = if let Some(Some(r)) = resize_adjustments.get(idx) {
|
|
||||||
a.left += r.left;
|
|
||||||
a.top += r.top;
|
|
||||||
a.right += r.right;
|
|
||||||
a.bottom += r.bottom;
|
|
||||||
a
|
|
||||||
} else {
|
|
||||||
*area
|
|
||||||
};
|
|
||||||
|
|
||||||
let half_width = area.right / 2;
|
|
||||||
let half_height = area.bottom / 2;
|
|
||||||
let half_resized_width = resized.right / 2;
|
|
||||||
let half_resized_height = resized.bottom / 2;
|
|
||||||
|
|
||||||
let (main_x, alt_x, alt_y, main_y);
|
|
||||||
|
|
||||||
if let Some(flip) = layout_flip {
|
|
||||||
match flip {
|
|
||||||
Flip::Horizontal => {
|
|
||||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
|
||||||
alt_x = resized.left;
|
|
||||||
|
|
||||||
alt_y = resized.top + half_resized_height;
|
|
||||||
main_y = resized.top;
|
|
||||||
}
|
|
||||||
Flip::Vertical => {
|
|
||||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
|
||||||
alt_y = resized.top;
|
|
||||||
|
|
||||||
main_x = resized.left;
|
|
||||||
alt_x = resized.left + half_resized_width;
|
|
||||||
}
|
|
||||||
Flip::HorizontalAndVertical => {
|
|
||||||
main_x = resized.left + half_width + (half_width - half_resized_width);
|
|
||||||
alt_x = resized.left;
|
|
||||||
main_y = resized.top + half_height + (half_height - half_resized_height);
|
|
||||||
alt_y = resized.top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
main_x = resized.left;
|
|
||||||
alt_x = resized.left + half_resized_width;
|
|
||||||
main_y = resized.top;
|
|
||||||
alt_y = resized.top + half_resized_height;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::if_not_else)]
|
|
||||||
if count == 0 {
|
|
||||||
vec![]
|
|
||||||
} else if count == 1 {
|
|
||||||
vec![Rect {
|
|
||||||
left: resized.left,
|
|
||||||
top: resized.top,
|
|
||||||
right: resized.right,
|
|
||||||
bottom: resized.bottom,
|
|
||||||
}]
|
|
||||||
} else if idx % 2 != 0 {
|
|
||||||
let mut res = vec![Rect {
|
|
||||||
left: resized.left,
|
|
||||||
top: main_y,
|
|
||||||
right: resized.right,
|
|
||||||
bottom: half_resized_height,
|
|
||||||
}];
|
|
||||||
res.append(&mut recursive_fibonacci(
|
|
||||||
idx + 1,
|
|
||||||
count - 1,
|
|
||||||
&Rect {
|
|
||||||
left: area.left,
|
|
||||||
top: alt_y,
|
|
||||||
right: area.right,
|
|
||||||
bottom: area.bottom - half_resized_height,
|
|
||||||
},
|
|
||||||
layout_flip,
|
|
||||||
resize_adjustments,
|
|
||||||
));
|
|
||||||
res
|
|
||||||
} else {
|
|
||||||
let mut res = vec![Rect {
|
|
||||||
left: main_x,
|
|
||||||
top: resized.top,
|
|
||||||
right: half_resized_width,
|
|
||||||
bottom: resized.bottom,
|
|
||||||
}];
|
|
||||||
res.append(&mut recursive_fibonacci(
|
|
||||||
idx + 1,
|
|
||||||
count - 1,
|
|
||||||
&Rect {
|
|
||||||
left: alt_x,
|
|
||||||
top: area.top,
|
|
||||||
right: area.right - half_resized_width,
|
|
||||||
bottom: area.bottom,
|
|
||||||
},
|
|
||||||
layout_flip,
|
|
||||||
resize_adjustments,
|
|
||||||
));
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,73 @@
|
|||||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||||
#![allow(clippy::missing_errors_doc)]
|
#![allow(clippy::missing_errors_doc, clippy::use_self)]
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use clap::ArgEnum;
|
use clap::ArgEnum;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
|
|
||||||
|
pub use arrangement::Arrangement;
|
||||||
|
pub use arrangement::Axis;
|
||||||
|
pub use custom_layout::CustomLayout;
|
||||||
pub use cycle_direction::CycleDirection;
|
pub use cycle_direction::CycleDirection;
|
||||||
pub use layout::Flip;
|
pub use default_layout::DefaultLayout;
|
||||||
|
pub use direction::Direction;
|
||||||
pub use layout::Layout;
|
pub use layout::Layout;
|
||||||
pub use operation_direction::OperationDirection;
|
pub use operation_direction::OperationDirection;
|
||||||
pub use rect::Rect;
|
pub use rect::Rect;
|
||||||
|
|
||||||
|
pub mod arrangement;
|
||||||
|
pub mod config_generation;
|
||||||
|
pub mod custom_layout;
|
||||||
pub mod cycle_direction;
|
pub mod cycle_direction;
|
||||||
|
pub mod default_layout;
|
||||||
|
pub mod direction;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod operation_direction;
|
pub mod operation_direction;
|
||||||
pub mod rect;
|
pub mod rect;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Display)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Display, JsonSchema)]
|
||||||
|
#[serde(tag = "type", content = "content")]
|
||||||
pub enum SocketMessage {
|
pub enum SocketMessage {
|
||||||
// Window / Container Commands
|
// Window / Container Commands
|
||||||
FocusWindow(OperationDirection),
|
FocusWindow(OperationDirection),
|
||||||
MoveWindow(OperationDirection),
|
MoveWindow(OperationDirection),
|
||||||
|
CycleFocusWindow(CycleDirection),
|
||||||
|
CycleMoveWindow(CycleDirection),
|
||||||
StackWindow(OperationDirection),
|
StackWindow(OperationDirection),
|
||||||
ResizeWindow(OperationDirection, Sizing),
|
ResizeWindowEdge(OperationDirection, Sizing),
|
||||||
|
ResizeWindowAxis(Axis, Sizing),
|
||||||
UnstackWindow,
|
UnstackWindow,
|
||||||
CycleStack(CycleDirection),
|
CycleStack(CycleDirection),
|
||||||
MoveContainerToMonitorNumber(usize),
|
MoveContainerToMonitorNumber(usize),
|
||||||
MoveContainerToWorkspaceNumber(usize),
|
MoveContainerToWorkspaceNumber(usize),
|
||||||
|
SendContainerToMonitorNumber(usize),
|
||||||
|
SendContainerToWorkspaceNumber(usize),
|
||||||
|
SendContainerToMonitorWorkspaceNumber(usize, usize),
|
||||||
|
MoveWorkspaceToMonitorNumber(usize),
|
||||||
Promote,
|
Promote,
|
||||||
ToggleFloat,
|
ToggleFloat,
|
||||||
ToggleMonocle,
|
ToggleMonocle,
|
||||||
ToggleMaximize,
|
ToggleMaximize,
|
||||||
|
ToggleWindowContainerBehaviour,
|
||||||
|
WindowHidingBehaviour(HidingBehaviour),
|
||||||
|
ToggleCrossMonitorMoveBehaviour,
|
||||||
|
CrossMonitorMoveBehaviour(MoveBehaviour),
|
||||||
|
UnmanagedWindowOperationBehaviour(OperationBehaviour),
|
||||||
// Current Workspace Commands
|
// Current Workspace Commands
|
||||||
ManageFocusedWindow,
|
ManageFocusedWindow,
|
||||||
UnmanageFocusedWindow,
|
UnmanageFocusedWindow,
|
||||||
AdjustContainerPadding(Sizing, i32),
|
AdjustContainerPadding(Sizing, i32),
|
||||||
AdjustWorkspacePadding(Sizing, i32),
|
AdjustWorkspacePadding(Sizing, i32),
|
||||||
ChangeLayout(Layout),
|
ChangeLayout(DefaultLayout),
|
||||||
FlipLayout(Flip),
|
ChangeLayoutCustom(PathBuf),
|
||||||
|
FlipLayout(Axis),
|
||||||
// Monitor and Workspace Commands
|
// Monitor and Workspace Commands
|
||||||
EnsureWorkspaces(usize, usize),
|
EnsureWorkspaces(usize, usize),
|
||||||
NewWorkspace,
|
NewWorkspace,
|
||||||
@@ -50,32 +75,55 @@ pub enum SocketMessage {
|
|||||||
Stop,
|
Stop,
|
||||||
TogglePause,
|
TogglePause,
|
||||||
Retile,
|
Retile,
|
||||||
|
QuickSave,
|
||||||
|
QuickLoad,
|
||||||
|
Save(PathBuf),
|
||||||
|
Load(PathBuf),
|
||||||
|
CycleFocusMonitor(CycleDirection),
|
||||||
|
CycleFocusWorkspace(CycleDirection),
|
||||||
FocusMonitorNumber(usize),
|
FocusMonitorNumber(usize),
|
||||||
FocusWorkspaceNumber(usize),
|
FocusWorkspaceNumber(usize),
|
||||||
|
FocusMonitorWorkspaceNumber(usize, usize),
|
||||||
ContainerPadding(usize, usize, i32),
|
ContainerPadding(usize, usize, i32),
|
||||||
WorkspacePadding(usize, usize, i32),
|
WorkspacePadding(usize, usize, i32),
|
||||||
WorkspaceTiling(usize, usize, bool),
|
WorkspaceTiling(usize, usize, bool),
|
||||||
WorkspaceName(usize, usize, String),
|
WorkspaceName(usize, usize, String),
|
||||||
WorkspaceLayout(usize, usize, Layout),
|
WorkspaceLayout(usize, usize, DefaultLayout),
|
||||||
|
WorkspaceLayoutCustom(usize, usize, PathBuf),
|
||||||
|
WorkspaceLayoutRule(usize, usize, usize, DefaultLayout),
|
||||||
|
WorkspaceLayoutCustomRule(usize, usize, usize, PathBuf),
|
||||||
|
ClearWorkspaceLayoutRules(usize, usize),
|
||||||
// Configuration
|
// Configuration
|
||||||
ReloadConfiguration,
|
ReloadConfiguration,
|
||||||
WatchConfiguration(bool),
|
WatchConfiguration(bool),
|
||||||
|
CompleteConfiguration,
|
||||||
|
ActiveWindowBorder(bool),
|
||||||
|
ActiveWindowBorderColour(u32, u32, u32),
|
||||||
|
InvisibleBorders(Rect),
|
||||||
|
WorkAreaOffset(Rect),
|
||||||
|
ResizeDelta(i32),
|
||||||
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||||
FloatRule(ApplicationIdentifier, String),
|
FloatRule(ApplicationIdentifier, String),
|
||||||
ManageRule(ApplicationIdentifier, String),
|
ManageRule(ApplicationIdentifier, String),
|
||||||
|
IdentifyObjectNameChangeApplication(ApplicationIdentifier, String),
|
||||||
IdentifyTrayApplication(ApplicationIdentifier, String),
|
IdentifyTrayApplication(ApplicationIdentifier, String),
|
||||||
|
IdentifyLayeredApplication(ApplicationIdentifier, String),
|
||||||
|
IdentifyBorderOverflowApplication(ApplicationIdentifier, String),
|
||||||
State,
|
State,
|
||||||
FocusFollowsMouse(bool),
|
Query(StateQuery),
|
||||||
|
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
|
||||||
|
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
|
||||||
|
MouseFollowsFocus(bool),
|
||||||
|
ToggleMouseFollowsFocus,
|
||||||
|
AddSubscriber(String),
|
||||||
|
RemoveSubscriber(String),
|
||||||
|
NotificationSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SocketMessage {
|
impl SocketMessage {
|
||||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
||||||
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
|
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
|
||||||
Ok(serde_json::from_slice(bytes)?)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for SocketMessage {
|
impl FromStr for SocketMessage {
|
||||||
@@ -86,15 +134,60 @@ impl FromStr for SocketMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum StateQuery {
|
||||||
|
FocusedMonitorIndex,
|
||||||
|
FocusedWorkspaceIndex,
|
||||||
|
FocusedContainerIndex,
|
||||||
|
FocusedWindowIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum ApplicationIdentifier {
|
pub enum ApplicationIdentifier {
|
||||||
Exe,
|
Exe,
|
||||||
Class,
|
Class,
|
||||||
Title,
|
Title,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum FocusFollowsMouseImplementation {
|
||||||
|
Komorebi,
|
||||||
|
Windows,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum WindowContainerBehaviour {
|
||||||
|
Create,
|
||||||
|
Append,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum MoveBehaviour {
|
||||||
|
Swap,
|
||||||
|
Insert,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum HidingBehaviour {
|
||||||
|
Hide,
|
||||||
|
Minimize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum OperationBehaviour {
|
||||||
|
Op,
|
||||||
|
NoOp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum Sizing {
|
pub enum Sizing {
|
||||||
Increase,
|
Increase,
|
||||||
@@ -105,8 +198,8 @@ impl Sizing {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
|
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Sizing::Increase => value + adjustment,
|
Self::Increase => value + adjustment,
|
||||||
Sizing::Decrease => {
|
Self::Decrease => {
|
||||||
if value > 0 && value - adjustment >= 0 {
|
if value > 0 && value - adjustment >= 0 {
|
||||||
value - adjustment
|
value - adjustment
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use clap::ArgEnum;
|
use clap::ArgEnum;
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
use strum::EnumString;
|
use strum::EnumString;
|
||||||
|
|
||||||
use crate::Flip;
|
use crate::direction::Direction;
|
||||||
use crate::Layout;
|
use crate::Axis;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum)]
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ArgEnum, JsonSchema)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum OperationDirection {
|
pub enum OperationDirection {
|
||||||
Left,
|
Left,
|
||||||
@@ -27,92 +30,35 @@ impl OperationDirection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flip_direction(direction: Self, layout_flip: Option<Flip>) -> Self {
|
fn flip(self, layout_flip: Option<Axis>) -> Self {
|
||||||
layout_flip.map_or(direction, |flip| match direction {
|
layout_flip.map_or(self, |flip| match self {
|
||||||
Self::Left => match flip {
|
Self::Left => match flip {
|
||||||
Flip::Horizontal | Flip::HorizontalAndVertical => Self::Right,
|
Axis::Horizontal | Axis::HorizontalAndVertical => Self::Right,
|
||||||
Flip::Vertical => direction,
|
Axis::Vertical => self,
|
||||||
},
|
},
|
||||||
Self::Right => match flip {
|
Self::Right => match flip {
|
||||||
Flip::Horizontal | Flip::HorizontalAndVertical => Self::Left,
|
Axis::Horizontal | Axis::HorizontalAndVertical => Self::Left,
|
||||||
Flip::Vertical => direction,
|
Axis::Vertical => self,
|
||||||
},
|
},
|
||||||
Self::Up => match flip {
|
Self::Up => match flip {
|
||||||
Flip::Vertical | Flip::HorizontalAndVertical => Self::Down,
|
Axis::Vertical | Axis::HorizontalAndVertical => Self::Down,
|
||||||
Flip::Horizontal => direction,
|
Axis::Horizontal => self,
|
||||||
},
|
},
|
||||||
Self::Down => match flip {
|
Self::Down => match flip {
|
||||||
Flip::Vertical | Flip::HorizontalAndVertical => Self::Up,
|
Axis::Vertical | Axis::HorizontalAndVertical => Self::Up,
|
||||||
Flip::Horizontal => direction,
|
Axis::Horizontal => self,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_valid(
|
pub fn destination(
|
||||||
self,
|
self,
|
||||||
layout: Layout,
|
layout: &dyn Direction,
|
||||||
layout_flip: Option<Flip>,
|
layout_flip: Option<Axis>,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
len: usize,
|
len: NonZeroUsize,
|
||||||
) -> bool {
|
) -> Option<usize> {
|
||||||
match Self::flip_direction(self, layout_flip) {
|
layout.index_in_direction(self.flip(layout_flip), idx, len.get())
|
||||||
OperationDirection::Up => match layout {
|
|
||||||
Layout::BSP => len > 2 && idx != 0 && idx != 1,
|
|
||||||
Layout::Columns => false,
|
|
||||||
Layout::Rows => idx != 0,
|
|
||||||
},
|
|
||||||
OperationDirection::Down => match layout {
|
|
||||||
Layout::BSP => len > 2 && idx != len - 1 && idx % 2 != 0,
|
|
||||||
Layout::Columns => false,
|
|
||||||
Layout::Rows => idx != len - 1,
|
|
||||||
},
|
|
||||||
OperationDirection::Left => match layout {
|
|
||||||
Layout::BSP => len > 1 && idx != 0,
|
|
||||||
Layout::Columns => idx != 0,
|
|
||||||
Layout::Rows => false,
|
|
||||||
},
|
|
||||||
OperationDirection::Right => match layout {
|
|
||||||
Layout::BSP => len > 1 && idx % 2 == 0 && idx != len - 1,
|
|
||||||
Layout::Columns => idx != len - 1,
|
|
||||||
Layout::Rows => false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn new_idx(self, layout: Layout, layout_flip: Option<Flip>, idx: usize) -> usize {
|
|
||||||
match Self::flip_direction(self, layout_flip) {
|
|
||||||
Self::Up => match layout {
|
|
||||||
Layout::BSP => {
|
|
||||||
if idx % 2 == 0 {
|
|
||||||
idx - 1
|
|
||||||
} else {
|
|
||||||
idx - 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Layout::Columns => unreachable!(),
|
|
||||||
Layout::Rows => idx - 1,
|
|
||||||
},
|
|
||||||
Self::Down => match layout {
|
|
||||||
Layout::BSP | Layout::Rows => idx + 1,
|
|
||||||
Layout::Columns => unreachable!(),
|
|
||||||
},
|
|
||||||
Self::Left => match layout {
|
|
||||||
Layout::BSP => {
|
|
||||||
if idx % 2 == 0 {
|
|
||||||
idx - 2
|
|
||||||
} else {
|
|
||||||
idx - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Layout::Columns => idx - 1,
|
|
||||||
Layout::Rows => unreachable!(),
|
|
||||||
},
|
|
||||||
Self::Right => match layout {
|
|
||||||
Layout::BSP | Layout::Columns => idx + 1,
|
|
||||||
Layout::Rows => unreachable!(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use windows::Win32::Foundation::RECT;
|
||||||
|
|
||||||
use bindings::Windows::Win32::Foundation::RECT;
|
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Eq, PartialEq)]
|
|
||||||
pub struct Rect {
|
pub struct Rect {
|
||||||
pub left: i32,
|
pub left: i32,
|
||||||
pub top: i32,
|
pub top: i32,
|
||||||
@@ -10,17 +11,6 @@ pub struct Rect {
|
|||||||
pub bottom: i32,
|
pub bottom: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Rect {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RECT> for Rect {
|
impl From<RECT> for Rect {
|
||||||
fn from(rect: RECT) -> Self {
|
fn from(rect: RECT) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
; Enable hot reloading of changes to this file
|
; Enable hot reloading of changes to this file
|
||||||
Run, komorebic.exe watch-configuration enable, , Hide
|
Run, komorebic.exe watch-configuration enable, , Hide
|
||||||
|
|
||||||
|
; Configure the invisible border dimensions
|
||||||
|
Run, komorebic.exe invisible-borders 7 0 14 7, , Hide
|
||||||
|
|
||||||
; Enable focus follows mouse
|
; Enable focus follows mouse
|
||||||
Run, komorebic.exe focus-follows-mouse enable, , Hide
|
Run, komorebic.exe focus-follows-mouse enable, , Hide
|
||||||
|
|
||||||
@@ -52,6 +55,9 @@ Run, komorebic.exe manage-rule exe TIM.exe, , Hide
|
|||||||
; Identify applications that close to the tray
|
; Identify applications that close to the tray
|
||||||
Run, komorebic.exe identify-tray-application exe Discord.exe, , Hide
|
Run, komorebic.exe identify-tray-application exe Discord.exe, , Hide
|
||||||
|
|
||||||
|
; Identify applications that have overflowing borders
|
||||||
|
Run, komorebic.exe identify-border-overflow exe Discord.exe, , Hide
|
||||||
|
|
||||||
; Change the focused window, Alt + Vim direction keys
|
; Change the focused window, Alt + Vim direction keys
|
||||||
!h::
|
!h::
|
||||||
Run, komorebic.exe focus left, , Hide
|
Run, komorebic.exe focus left, , Hide
|
||||||
|
|||||||
243
komorebi.sample.with.lib.ahk
Normal file
243
komorebi.sample.with.lib.ahk
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
#SingleInstance Force
|
||||||
|
#Include %A_ScriptDir%\komorebic.lib.ahk
|
||||||
|
|
||||||
|
; Default to minimizing windows when switching workspaces
|
||||||
|
WindowHidingBehaviour("minimize")
|
||||||
|
|
||||||
|
; Enable hot reloading of changes to this file
|
||||||
|
WatchConfiguration("enable")
|
||||||
|
|
||||||
|
; Ensure there are 5 workspaces created on monitor 0
|
||||||
|
EnsureWorkspaces(0, 5)
|
||||||
|
|
||||||
|
; Configure the invisible border dimensions
|
||||||
|
InvisibleBorders(7, 0, 14, 7)
|
||||||
|
|
||||||
|
; Configure the 1st workspace
|
||||||
|
WorkspaceName(0, 0, "bsp")
|
||||||
|
|
||||||
|
; Configure the 2nd workspace
|
||||||
|
WorkspaceName(0, 1, "columns") ; Optionally set the name of the workspace
|
||||||
|
WorkspacePadding(0, 1, 30) ; Set the padding around the edge of the screen
|
||||||
|
ContainerPadding(0, 1, 30) ; Set the padding between the containers on the screen
|
||||||
|
WorkspaceRule("exe", "slack.exe", 0, 1) ; Always show chat apps on this workspace
|
||||||
|
|
||||||
|
; Configure the 3rd workspace
|
||||||
|
WorkspaceName(0, 2, "thicc")
|
||||||
|
WorkspacePadding(0, 2, 200) ; Set some super thicc padding
|
||||||
|
|
||||||
|
; Configure the 4th workspace
|
||||||
|
WorkspaceName(0, 3, "matrix")
|
||||||
|
WorkspacePadding(0, 3, 0) ; No padding at all
|
||||||
|
ContainerPadding(0, 3, 0) ; Matrix-y hacker vibes
|
||||||
|
|
||||||
|
; Configure the 5th workspace
|
||||||
|
WorkspaceName(0, 4, "floaty")
|
||||||
|
WorkspaceTiling(0, 4, "disable") ; Everything floats here
|
||||||
|
|
||||||
|
; Configure floating rules
|
||||||
|
FloatRule("class", "SunAwtDialog") ; All the IntelliJ popups
|
||||||
|
FloatRule("title", "Control Panel")
|
||||||
|
FloatRule("class", "TaskManagerWindow")
|
||||||
|
FloatRule("exe", "Wally.exe")
|
||||||
|
FloatRule("exe", "wincompose.exe")
|
||||||
|
FloatRule("exe", "1Password.exe")
|
||||||
|
FloatRule("exe", "Wox.exe")
|
||||||
|
FloatRule("exe", "ddm.exe")
|
||||||
|
FloatRule("class", "Chrome_RenderWidgetHostHWND") ; GOG Electron invisible overlay
|
||||||
|
FloatRule("class", "CEFCLIENT")
|
||||||
|
|
||||||
|
; Identify Minimize-to-Tray Applications
|
||||||
|
IdentifyTrayApplication("exe", "Discord.exe")
|
||||||
|
IdentifyTrayApplication("exe", "Spotify.exe")
|
||||||
|
IdentifyTrayApplication("exe", "GalaxyClient.exe")
|
||||||
|
|
||||||
|
; Identify Electron applications with overflowing borders
|
||||||
|
IdentifyBorderOverflow("exe", "Discord.exe")
|
||||||
|
IdentifyBorderOverflow("exe", "Spotify.exe")
|
||||||
|
IdentifyBorderOverflow("exe", "GalaxyClient.exe")
|
||||||
|
IdentifyBorderOverflow("class", "ZPFTEWndClass")
|
||||||
|
|
||||||
|
; Identify applications to be forcibly managed
|
||||||
|
ManageRule("exe", "GalaxyClient.exe")
|
||||||
|
|
||||||
|
; Change the focused window, Alt + Vim direction keys
|
||||||
|
!h::
|
||||||
|
Focus("left")
|
||||||
|
return
|
||||||
|
|
||||||
|
!j::
|
||||||
|
Focus("down")
|
||||||
|
return
|
||||||
|
|
||||||
|
!k::
|
||||||
|
Focus("up")
|
||||||
|
return
|
||||||
|
|
||||||
|
!l::
|
||||||
|
Focus("right")
|
||||||
|
return
|
||||||
|
|
||||||
|
; Move the focused window in a given direction, Alt + Shift + Vim direction keys
|
||||||
|
!+h::
|
||||||
|
Move("left")
|
||||||
|
return
|
||||||
|
|
||||||
|
!+j::
|
||||||
|
Move("down")
|
||||||
|
return
|
||||||
|
|
||||||
|
!+k::
|
||||||
|
Move("up")
|
||||||
|
return
|
||||||
|
|
||||||
|
!+l::
|
||||||
|
Move("right")
|
||||||
|
return
|
||||||
|
|
||||||
|
; Stack the focused window in a given direction, Alt + Shift + direction keys
|
||||||
|
!+Left::
|
||||||
|
Stack("left")
|
||||||
|
return
|
||||||
|
|
||||||
|
!+Down::
|
||||||
|
Stack("down")
|
||||||
|
return
|
||||||
|
|
||||||
|
!+Up::
|
||||||
|
Stack("up")
|
||||||
|
return
|
||||||
|
|
||||||
|
!+Right::
|
||||||
|
Stack("right")
|
||||||
|
return
|
||||||
|
|
||||||
|
!]::
|
||||||
|
CycleStack("next")
|
||||||
|
return
|
||||||
|
|
||||||
|
![::
|
||||||
|
CycleStack("previous")
|
||||||
|
return
|
||||||
|
|
||||||
|
; Unstack the focused window, Alt + Shift + D
|
||||||
|
!+d::
|
||||||
|
Unstack()
|
||||||
|
return
|
||||||
|
|
||||||
|
; Promote the focused window to the top of the tree, Alt + Shift + Enter
|
||||||
|
!+Enter::
|
||||||
|
Promote()
|
||||||
|
return
|
||||||
|
|
||||||
|
; Manage the focused window
|
||||||
|
!=::
|
||||||
|
Manage()
|
||||||
|
return
|
||||||
|
|
||||||
|
; Unmanage the focused window
|
||||||
|
!-::
|
||||||
|
Unmanage()
|
||||||
|
return
|
||||||
|
|
||||||
|
; Switch to an equal-width, max-height column layout on the main workspace, Alt + Shift + C
|
||||||
|
!+c::
|
||||||
|
ChangeLayout("columns")
|
||||||
|
return
|
||||||
|
|
||||||
|
; Switch to the default bsp tiling layout on the main workspace, Alt + Shift + T
|
||||||
|
!+t::
|
||||||
|
ChangeLayout("bsp")
|
||||||
|
return
|
||||||
|
|
||||||
|
; Toggle the Monocle layout for the focused window, Alt + Shift + F
|
||||||
|
!+f::
|
||||||
|
ToggleMonocle()
|
||||||
|
return
|
||||||
|
|
||||||
|
; Toggle native maximize for the focused window, Alt + Shift + =
|
||||||
|
!+=::
|
||||||
|
ToggleMaximize()
|
||||||
|
return
|
||||||
|
|
||||||
|
; Flip horizontally, Alt + X
|
||||||
|
!x::
|
||||||
|
FlipLayout("horizontal")
|
||||||
|
return
|
||||||
|
|
||||||
|
; Flip vertically, Alt + Y
|
||||||
|
!y::
|
||||||
|
FlipLayout("vertical")
|
||||||
|
return
|
||||||
|
|
||||||
|
; Force a retile if things get janky, Alt + Shift + R
|
||||||
|
!+r::
|
||||||
|
Retile()
|
||||||
|
return
|
||||||
|
|
||||||
|
; Float the focused window, Alt + T
|
||||||
|
!t::
|
||||||
|
ToggleFloat()
|
||||||
|
return
|
||||||
|
|
||||||
|
; Reload ~/komorebi.ahk, Alt + O
|
||||||
|
!o::
|
||||||
|
ReloadConfiguration()
|
||||||
|
return
|
||||||
|
|
||||||
|
; Pause responding to any window events or komorebic commands, Alt + P
|
||||||
|
!p::
|
||||||
|
TogglePause()
|
||||||
|
return
|
||||||
|
|
||||||
|
; Enable focus follows mouse
|
||||||
|
!0::
|
||||||
|
ToggleFocusFollowsMouse("komorebi")
|
||||||
|
return
|
||||||
|
|
||||||
|
; Switch to workspace
|
||||||
|
!1::
|
||||||
|
Send !
|
||||||
|
FocusWorkspace(0)
|
||||||
|
return
|
||||||
|
|
||||||
|
!2::
|
||||||
|
Send !
|
||||||
|
FocusWorkspace(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
!3::
|
||||||
|
Send !
|
||||||
|
FocusWorkspace(2)
|
||||||
|
return
|
||||||
|
|
||||||
|
!4::
|
||||||
|
Send !
|
||||||
|
FocusWorkspace(3)
|
||||||
|
return
|
||||||
|
|
||||||
|
!5::
|
||||||
|
Send !
|
||||||
|
FocusWorkspace(4)
|
||||||
|
return
|
||||||
|
|
||||||
|
; Move window to workspace
|
||||||
|
!+1::
|
||||||
|
MoveToWorkspace(0)
|
||||||
|
return
|
||||||
|
|
||||||
|
!+2::
|
||||||
|
MoveToWorkspace(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
!+3::
|
||||||
|
MoveToWorkspace(2)
|
||||||
|
return
|
||||||
|
|
||||||
|
!+4::
|
||||||
|
MoveToWorkspace(3)
|
||||||
|
return
|
||||||
|
|
||||||
|
!+5::
|
||||||
|
MoveToWorkspace(4)
|
||||||
|
return
|
||||||
@@ -1,37 +1,62 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebi"
|
name = "komorebi"
|
||||||
version = "0.1.1"
|
version = "0.1.11"
|
||||||
edition = "2018"
|
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||||
|
description = "A tiling window manager for Windows"
|
||||||
|
categories = ["tiling-window-manager", "windows"]
|
||||||
|
repository = "https://github.com/LGUG2Z/komorebi"
|
||||||
|
license = "MIT"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bindings = { package = "bindings", path = "../bindings" }
|
|
||||||
komorebi-core = { path = "../komorebi-core" }
|
komorebi-core = { path = "../komorebi-core" }
|
||||||
|
|
||||||
bitflags = "1"
|
bitflags = "1"
|
||||||
color-eyre = "0.5"
|
clap = { version = "3", features = ["derive"] }
|
||||||
|
color-eyre = "0.6"
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
crossbeam-utils = "0.8"
|
crossbeam-utils = "0.8"
|
||||||
ctrlc = "3"
|
ctrlc = "3"
|
||||||
dirs = "3"
|
dirs = "4"
|
||||||
eyre = "0.6"
|
|
||||||
getset = "0.1"
|
getset = "0.1"
|
||||||
hotwatch = "0.4"
|
hotwatch = "0.4"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
|
miow = "0.4"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
|
os_info = "3.4"
|
||||||
|
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||||
paste = "1"
|
paste = "1"
|
||||||
|
schemars = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
strum = { version = "0.21", features = ["derive"] }
|
strum = { version = "0.24", features = ["derive"] }
|
||||||
sysinfo = "0.20"
|
sysinfo = "0.25"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-appender = "0.1"
|
tracing-appender = "0.2"
|
||||||
tracing-subscriber = "0.2"
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
uds_windows = "1"
|
uds_windows = "1"
|
||||||
which = "4"
|
which = "4"
|
||||||
winvd = "0.0.20"
|
winput = "0.2"
|
||||||
|
winreg = "0.10"
|
||||||
|
|
||||||
|
[dependencies.windows]
|
||||||
|
version = "0.39"
|
||||||
|
features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Graphics_Dwm",
|
||||||
|
"Win32_Graphics_Gdi",
|
||||||
|
"Win32_System_LibraryLoader",
|
||||||
|
"Win32_System_RemoteDesktop",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_UI_Accessibility",
|
||||||
|
"Win32_UI_HiDpi",
|
||||||
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
|
"Win32_UI_Shell",
|
||||||
|
"Win32_UI_Shell_Common",
|
||||||
|
"Win32_UI_WindowsAndMessaging"
|
||||||
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
deadlock_detection = []
|
deadlock_detection = []
|
||||||
|
|||||||
119
komorebi/src/border.rs
Normal file
119
komorebi/src/border.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use komorebi_core::Rect;
|
||||||
|
use windows::core::PCSTR;
|
||||||
|
use windows::Win32::Foundation::HWND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageA;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::FindWindowA;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetMessageA;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSA;
|
||||||
|
|
||||||
|
use crate::window::Window;
|
||||||
|
use crate::windows_callbacks;
|
||||||
|
use crate::WindowsApi;
|
||||||
|
use crate::BORDER_HWND;
|
||||||
|
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||||
|
use crate::WINDOWS_11;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Border {
|
||||||
|
pub(crate) hwnd: isize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<isize> for Border {
|
||||||
|
fn from(hwnd: isize) -> Self {
|
||||||
|
Self { hwnd }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Border {
|
||||||
|
pub const fn hwnd(self) -> HWND {
|
||||||
|
HWND(self.hwnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(name: &str) -> Result<()> {
|
||||||
|
let name = format!("{name}\0");
|
||||||
|
let instance = WindowsApi::module_handle_w()?;
|
||||||
|
let class_name = PCSTR(name.as_ptr());
|
||||||
|
let brush = WindowsApi::create_solid_brush(255, 140, 0);
|
||||||
|
let window_class = WNDCLASSA {
|
||||||
|
hInstance: instance,
|
||||||
|
lpszClassName: class_name,
|
||||||
|
style: CS_HREDRAW | CS_VREDRAW,
|
||||||
|
lpfnWndProc: Some(windows_callbacks::border_window),
|
||||||
|
hbrBackground: brush,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let _atom = WindowsApi::register_class_a(&window_class)?;
|
||||||
|
|
||||||
|
let name_cl = name.clone();
|
||||||
|
std::thread::spawn(move || -> Result<()> {
|
||||||
|
let hwnd = WindowsApi::create_border_window(PCSTR(name_cl.as_ptr()), instance)?;
|
||||||
|
let border = Self::from(hwnd);
|
||||||
|
|
||||||
|
let mut message = MSG::default();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
while GetMessageA(&mut message, border.hwnd(), 0, 0).into() {
|
||||||
|
DispatchMessageA(&message);
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut hwnd = HWND(0);
|
||||||
|
while hwnd == HWND(0) {
|
||||||
|
hwnd = unsafe { FindWindowA(PCSTR(name.as_ptr()), PCSTR::null()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
BORDER_HWND.store(hwnd.0, Ordering::SeqCst);
|
||||||
|
|
||||||
|
if *WINDOWS_11 {
|
||||||
|
WindowsApi::round_corners(hwnd.0)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide(self) -> Result<()> {
|
||||||
|
WindowsApi::hide_border_window(self.hwnd())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_position(
|
||||||
|
self,
|
||||||
|
window: Window,
|
||||||
|
invisible_borders: &Rect,
|
||||||
|
activate: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut should_expand_border = false;
|
||||||
|
|
||||||
|
let mut rect = WindowsApi::window_rect(window.hwnd())?;
|
||||||
|
rect.top -= invisible_borders.bottom;
|
||||||
|
rect.bottom += invisible_borders.bottom;
|
||||||
|
|
||||||
|
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||||
|
if border_overflows.contains(&window.title()?)
|
||||||
|
|| border_overflows.contains(&window.exe()?)
|
||||||
|
|| border_overflows.contains(&window.class()?)
|
||||||
|
{
|
||||||
|
should_expand_border = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if should_expand_border {
|
||||||
|
rect.left -= invisible_borders.left;
|
||||||
|
rect.top -= invisible_borders.top;
|
||||||
|
rect.right += invisible_borders.right;
|
||||||
|
rect.bottom += invisible_borders.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowsApi::position_border_window(self.hwnd(), &rect, activate)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,13 @@ use std::collections::VecDeque;
|
|||||||
|
|
||||||
use getset::Getters;
|
use getset::Getters;
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::ring::Ring;
|
use crate::ring::Ring;
|
||||||
use crate::window::Window;
|
use crate::window::Window;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Getters)]
|
#[derive(Debug, Clone, Serialize, Getters, JsonSchema)]
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
#[getset(get = "pub")]
|
#[getset(get = "pub")]
|
||||||
@@ -44,6 +45,18 @@ impl Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
|
||||||
|
for window in self.windows() {
|
||||||
|
if let Ok(window_exe) = window.exe() {
|
||||||
|
if exe == window_exe {
|
||||||
|
return Option::from(window.hwnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn contains_window(&self, hwnd: isize) -> bool {
|
pub fn contains_window(&self, hwnd: isize) -> bool {
|
||||||
for window in self.windows() {
|
for window in self.windows() {
|
||||||
if window.hwnd == hwnd {
|
if window.hwnd == hwnd {
|
||||||
@@ -66,18 +79,18 @@ impl Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_window_by_idx(&mut self, idx: usize) -> Option<Window> {
|
pub fn remove_window_by_idx(&mut self, idx: usize) -> Option<Window> {
|
||||||
self.windows_mut().remove(idx)
|
let window = self.windows_mut().remove(idx);
|
||||||
|
|
||||||
|
if idx != 0 {
|
||||||
|
self.focus_window(idx - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
window
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_focused_window(&mut self) -> Option<Window> {
|
pub fn remove_focused_window(&mut self) -> Option<Window> {
|
||||||
let focused_idx = self.focused_window_idx();
|
let focused_idx = self.focused_window_idx();
|
||||||
let window = self.remove_window_by_idx(focused_idx);
|
self.remove_window_by_idx(focused_idx)
|
||||||
|
|
||||||
if focused_idx != 0 {
|
|
||||||
self.focus_window(focused_idx - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
window
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_window(&mut self, window: Window) {
|
pub fn add_window(&mut self, window: Window) {
|
||||||
|
|||||||
@@ -1,30 +1,50 @@
|
|||||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||||
#![allow(clippy::missing_errors_doc)]
|
#![allow(clippy::missing_errors_doc, clippy::redundant_pub_crate)]
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::atomic::AtomicIsize;
|
||||||
|
use std::sync::atomic::AtomicU32;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[cfg(feature = "deadlock_detection")]
|
#[cfg(feature = "deadlock_detection")]
|
||||||
use std::thread;
|
|
||||||
#[cfg(feature = "deadlock_detection")]
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use color_eyre::eyre::ContextCompat;
|
use clap::Parser;
|
||||||
|
use color_eyre::eyre::anyhow;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
|
use crossbeam_utils::Backoff;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use os_info::Version;
|
||||||
#[cfg(feature = "deadlock_detection")]
|
#[cfg(feature = "deadlock_detection")]
|
||||||
use parking_lot::deadlock;
|
use parking_lot::deadlock;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Serialize;
|
||||||
|
use sysinfo::Process;
|
||||||
|
use sysinfo::ProcessExt;
|
||||||
use sysinfo::SystemExt;
|
use sysinfo::SystemExt;
|
||||||
use tracing_appender::non_blocking::WorkerGuard;
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use which::which;
|
use which::which;
|
||||||
|
use winreg::enums::HKEY_CURRENT_USER;
|
||||||
|
use winreg::RegKey;
|
||||||
|
|
||||||
|
use komorebi_core::HidingBehaviour;
|
||||||
|
use komorebi_core::SocketMessage;
|
||||||
|
|
||||||
|
use crate::border::Border;
|
||||||
use crate::process_command::listen_for_commands;
|
use crate::process_command::listen_for_commands;
|
||||||
use crate::process_event::listen_for_events;
|
use crate::process_event::listen_for_events;
|
||||||
|
use crate::process_movement::listen_for_movements;
|
||||||
|
use crate::window_manager::State;
|
||||||
use crate::window_manager::WindowManager;
|
use crate::window_manager::WindowManager;
|
||||||
use crate::window_manager_event::WindowManagerEvent;
|
use crate::window_manager_event::WindowManagerEvent;
|
||||||
use crate::windows_api::WindowsApi;
|
use crate::windows_api::WindowsApi;
|
||||||
@@ -32,10 +52,12 @@ use crate::windows_api::WindowsApi;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod ring;
|
mod ring;
|
||||||
|
|
||||||
|
mod border;
|
||||||
mod container;
|
mod container;
|
||||||
mod monitor;
|
mod monitor;
|
||||||
mod process_command;
|
mod process_command;
|
||||||
mod process_event;
|
mod process_event;
|
||||||
|
mod process_movement;
|
||||||
mod set_window_position;
|
mod set_window_position;
|
||||||
mod styles;
|
mod styles;
|
||||||
mod window;
|
mod window;
|
||||||
@@ -49,18 +71,17 @@ mod workspace;
|
|||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||||
static ref LAYERED_EXE_WHITELIST: Arc<Mutex<Vec<String>>> =
|
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<String>>> =
|
||||||
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
|
Arc::new(Mutex::new(vec!["steam.exe".to_string()]));
|
||||||
static ref TRAY_AND_MULTI_WINDOW_CLASSES: Arc<Mutex<Vec<String>>> =
|
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> =
|
||||||
Arc::new(Mutex::new(vec![]));
|
Arc::new(Mutex::new(vec![
|
||||||
static ref TRAY_AND_MULTI_WINDOW_EXES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
"explorer.exe".to_string(),
|
||||||
"explorer.exe".to_string(),
|
"firefox.exe".to_string(),
|
||||||
"firefox.exe".to_string(),
|
"chrome.exe".to_string(),
|
||||||
"chrome.exe".to_string(),
|
"idea64.exe".to_string(),
|
||||||
"idea64.exe".to_string(),
|
"ApplicationFrameHost.exe".to_string(),
|
||||||
"ApplicationFrameHost.exe".to_string(),
|
"steam.exe".to_string(),
|
||||||
"steam.exe".to_string(),
|
]));
|
||||||
]));
|
|
||||||
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||||
"firefox.exe".to_string(),
|
"firefox.exe".to_string(),
|
||||||
"idea64.exe".to_string(),
|
"idea64.exe".to_string(),
|
||||||
@@ -68,9 +89,77 @@ lazy_static! {
|
|||||||
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, (usize, usize)>>> =
|
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, (usize, usize)>>> =
|
||||||
Arc::new(Mutex::new(HashMap::new()));
|
Arc::new(Mutex::new(HashMap::new()));
|
||||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||||
|
// mstsc.exe creates these on Windows 11 when a WSL process is launched
|
||||||
|
// https://github.com/LGUG2Z/komorebi/issues/74
|
||||||
|
"OPContainerClass".to_string(),
|
||||||
|
"IHWindowClass".to_string()
|
||||||
|
]));
|
||||||
|
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||||
|
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||||
|
"X410.exe".to_string(),
|
||||||
|
"mstsc.exe".to_string(),
|
||||||
|
"vcxsrv.exe".to_string(),
|
||||||
|
]));
|
||||||
|
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
|
||||||
|
Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
|
||||||
|
Arc::new(Mutex::new(HidingBehaviour::Minimize));
|
||||||
|
static ref HOME_DIR: PathBuf = {
|
||||||
|
if let Ok(home_path) = std::env::var("KOMOREBI_CONFIG_HOME") {
|
||||||
|
let home = PathBuf::from(&home_path);
|
||||||
|
|
||||||
|
if home.as_path().is_dir() {
|
||||||
|
home
|
||||||
|
} else {
|
||||||
|
panic!(
|
||||||
|
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||||
|
home_path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dirs::home_dir().expect("there is no home directory")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
|
||||||
|
static ref AHK_V1_EXE: String = {
|
||||||
|
let mut ahk_v1: String = String::from("autohotkey.exe");
|
||||||
|
|
||||||
|
if let Ok(komorebi_ahk_v1_exe) = std::env::var("KOMOREBI_AHK_V1_EXE") {
|
||||||
|
if which(&komorebi_ahk_v1_exe).is_ok() {
|
||||||
|
ahk_v1 = komorebi_ahk_v1_exe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ahk_v1
|
||||||
|
};
|
||||||
|
static ref AHK_V2_EXE: String = {
|
||||||
|
let mut ahk_v2: String = String::from("AutoHotkey64.exe");
|
||||||
|
|
||||||
|
if let Ok(komorebi_ahk_v2_exe) = std::env::var("KOMOREBI_AHK_V2_EXE") {
|
||||||
|
if which(&komorebi_ahk_v2_exe).is_ok() {
|
||||||
|
ahk_v2 = komorebi_ahk_v2_exe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ahk_v2
|
||||||
|
};
|
||||||
|
|
||||||
|
static ref WINDOWS_11: bool = {
|
||||||
|
matches!(
|
||||||
|
os_info::get().version(),
|
||||||
|
Version::Semantic(_, _, x) if x >= &22000
|
||||||
|
)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
|
||||||
|
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||||
|
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
|
||||||
|
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||||
|
pub static BORDER_HWND: AtomicIsize = AtomicIsize::new(0);
|
||||||
|
pub static BORDER_HIDDEN: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||||
@@ -82,8 +171,7 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
|||||||
std::env::set_var("RUST_LOG", "info");
|
std::env::set_var("RUST_LOG", "info");
|
||||||
}
|
}
|
||||||
|
|
||||||
let home = dirs::home_dir().context("there is no home directory")?;
|
let appender = tracing_appender::rolling::never(DATA_DIR.clone(), "komorebi.log");
|
||||||
let appender = tracing_appender::rolling::never(home, "komorebi.log");
|
|
||||||
let color_appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi.log");
|
let color_appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi.log");
|
||||||
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
|
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
|
||||||
let (color_non_blocking, color_guard) = tracing_appender::non_blocking(color_appender);
|
let (color_non_blocking, color_guard) = tracing_appender::non_blocking(color_appender);
|
||||||
@@ -113,27 +201,30 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
|||||||
// occurred to be recorded.
|
// occurred to be recorded.
|
||||||
std::panic::set_hook(Box::new(|panic| {
|
std::panic::set_hook(Box::new(|panic| {
|
||||||
// If the panic has a source location, record it as structured fields.
|
// If the panic has a source location, record it as structured fields.
|
||||||
if let Some(location) = panic.location() {
|
panic.location().map_or_else(
|
||||||
// On nightly Rust, where the `PanicInfo` type also exposes a
|
|| {
|
||||||
// `message()` method returning just the message, we could record
|
tracing::error!(message = %panic);
|
||||||
// just the message instead of the entire `fmt::Display`
|
},
|
||||||
// implementation, avoiding the duplciated location
|
|location| {
|
||||||
tracing::error!(
|
// On nightly Rust, where the `PanicInfo` type also exposes a
|
||||||
message = %panic,
|
// `message()` method returning just the message, we could record
|
||||||
panic.file = location.file(),
|
// just the message instead of the entire `fmt::Display`
|
||||||
panic.line = location.line(),
|
// implementation, avoiding the duplciated location
|
||||||
panic.column = location.column(),
|
tracing::error!(
|
||||||
);
|
message = %panic,
|
||||||
} else {
|
panic.file = location.file(),
|
||||||
tracing::error!(message = %panic);
|
panic.line = location.line(),
|
||||||
}
|
panic.column = location.column(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Ok((guard, color_guard))
|
Ok((guard, color_guard))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_configuration() -> Result<()> {
|
pub fn load_configuration() -> Result<()> {
|
||||||
let home = dirs::home_dir().context("there is no home directory")?;
|
let home = HOME_DIR.clone();
|
||||||
|
|
||||||
let mut config_v1 = home.clone();
|
let mut config_v1 = home.clone();
|
||||||
config_v1.push("komorebi.ahk");
|
config_v1.push("komorebi.ahk");
|
||||||
@@ -141,42 +232,134 @@ pub fn load_configuration() -> Result<()> {
|
|||||||
let mut config_v2 = home;
|
let mut config_v2 = home;
|
||||||
config_v2.push("komorebi.ahk2");
|
config_v2.push("komorebi.ahk2");
|
||||||
|
|
||||||
if config_v1.exists() && which("autohotkey.exe").is_ok() {
|
if config_v1.exists() && which(&*AHK_V1_EXE).is_ok() {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"loading configuration file: {}",
|
"loading configuration file: {}",
|
||||||
config_v1
|
config_v1
|
||||||
.as_os_str()
|
.as_os_str()
|
||||||
.to_str()
|
.to_str()
|
||||||
.context("cannot convert path to string")?
|
.ok_or_else(|| anyhow!("cannot convert path to string"))?
|
||||||
);
|
);
|
||||||
|
|
||||||
Command::new("autohotkey.exe")
|
Command::new("autohotkey.exe")
|
||||||
.arg(config_v1.as_os_str())
|
.arg(config_v1.as_os_str())
|
||||||
.output()?;
|
.output()?;
|
||||||
} else if config_v2.exists() && which("AutoHotkey64.exe").is_ok() {
|
} else if config_v2.exists() && which(&*AHK_V2_EXE).is_ok() {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"loading configuration file: {}",
|
"loading configuration file: {}",
|
||||||
config_v2
|
config_v2
|
||||||
.as_os_str()
|
.as_os_str()
|
||||||
.to_str()
|
.to_str()
|
||||||
.context("cannot convert path to string")?
|
.ok_or_else(|| anyhow!("cannot convert path to string"))?
|
||||||
);
|
);
|
||||||
|
|
||||||
Command::new("AutoHotkey64.exe")
|
Command::new("AutoHotkey64.exe")
|
||||||
.arg(config_v2.as_os_str())
|
.arg(config_v2.as_os_str())
|
||||||
.output()?;
|
.output()?;
|
||||||
|
} else {
|
||||||
|
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
|
||||||
|
// This is the path on Windows 10
|
||||||
|
let mut current = hkcu
|
||||||
|
.open_subkey(format!(
|
||||||
|
r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#,
|
||||||
|
SESSION_ID.load(Ordering::SeqCst)
|
||||||
|
))
|
||||||
|
.ok()
|
||||||
|
.and_then(
|
||||||
|
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||||
|
Ok(current) => Option::from(current.bytes),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is the path on Windows 11
|
||||||
|
if current.is_none() {
|
||||||
|
current = hkcu
|
||||||
|
.open_subkey(r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops"#)
|
||||||
|
.ok()
|
||||||
|
.and_then(
|
||||||
|
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||||
|
Ok(current) => Option::from(current.bytes),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not
|
||||||
|
// exist until one has been created in the task view
|
||||||
|
|
||||||
|
// The registry value will also not exist on user login if virtual desktops have been created
|
||||||
|
// but the task view has not been initiated
|
||||||
|
|
||||||
|
// In both of these cases, we return None, and the virtual desktop validation will never run. In
|
||||||
|
// the latter case, if the user desires this validation after initiating the task view, komorebi
|
||||||
|
// should be restarted, and then when this // fn runs again for the first time, it will pick up
|
||||||
|
// the value of CurrentVirtualDesktop and validate against it accordingly
|
||||||
|
current
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum NotificationEvent {
|
||||||
|
WindowManager(WindowManagerEvent),
|
||||||
|
Socket(SocketMessage),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, JsonSchema)]
|
||||||
|
pub struct Notification {
|
||||||
|
pub event: NotificationEvent,
|
||||||
|
pub state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||||
|
let mut stale_subscriptions = vec![];
|
||||||
|
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
|
||||||
|
for (subscriber, pipe) in subscriptions.iter_mut() {
|
||||||
|
match writeln!(pipe, "{}", notification) {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::debug!("pushed notification to subscriber: {}", subscriber);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
// ERROR_FILE_NOT_FOUND
|
||||||
|
// 2 (0x2)
|
||||||
|
// The system cannot find the file specified.
|
||||||
|
|
||||||
|
// ERROR_NO_DATA
|
||||||
|
// 232 (0xE8)
|
||||||
|
// The pipe is being closed.
|
||||||
|
|
||||||
|
// Remove the subscription; the process will have to subscribe again
|
||||||
|
if let Some(2 | 232) = error.raw_os_error() {
|
||||||
|
let subscriber_cl = subscriber.clone();
|
||||||
|
stale_subscriptions.push(subscriber_cl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for subscriber in stale_subscriptions {
|
||||||
|
tracing::warn!("removing stale subscription: {}", subscriber);
|
||||||
|
subscriptions.remove(&subscriber);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "deadlock_detection")]
|
#[cfg(feature = "deadlock_detection")]
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
fn detect_deadlocks() {
|
fn detect_deadlocks() {
|
||||||
// Create a background thread which checks for deadlocks every 10s
|
// Create a background thread which checks for deadlocks every 10s
|
||||||
thread::spawn(move || loop {
|
std::thread::spawn(move || loop {
|
||||||
tracing::info!("running deadlock detector");
|
tracing::info!("running deadlock detector");
|
||||||
thread::sleep(Duration::from_secs(5));
|
std::thread::sleep(Duration::from_secs(5));
|
||||||
let deadlocks = deadlock::check_deadlock();
|
let deadlocks = deadlock::check_deadlock();
|
||||||
if deadlocks.is_empty() {
|
if deadlocks.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
@@ -193,61 +376,109 @@ fn detect_deadlocks() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[clap(author, about, version)]
|
||||||
|
struct Opts {
|
||||||
|
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||||
|
#[clap(action, short, long = "ffm")]
|
||||||
|
focus_follows_mouse: bool,
|
||||||
|
/// Wait for 'komorebic complete-configuration' to be sent before processing events
|
||||||
|
#[clap(action, short, long)]
|
||||||
|
await_configuration: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
match std::env::args().count() {
|
let opts: Opts = Opts::parse();
|
||||||
1 => {
|
CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst);
|
||||||
let mut system = sysinfo::System::new_all();
|
|
||||||
system.refresh_processes();
|
|
||||||
|
|
||||||
if system.process_by_name("komorebi.exe").len() > 1 {
|
let arg_count = std::env::args().count();
|
||||||
|
let has_valid_args = arg_count == 1
|
||||||
|
|| (arg_count == 2 && (opts.await_configuration || opts.focus_follows_mouse))
|
||||||
|
|| (arg_count == 3 && opts.await_configuration && opts.focus_follows_mouse);
|
||||||
|
|
||||||
|
if has_valid_args {
|
||||||
|
let session_id = WindowsApi::process_id_to_session_id()?;
|
||||||
|
SESSION_ID.store(session_id, Ordering::SeqCst);
|
||||||
|
|
||||||
|
let mut system = sysinfo::System::new_all();
|
||||||
|
system.refresh_processes();
|
||||||
|
|
||||||
|
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe").collect();
|
||||||
|
|
||||||
|
if matched_procs.len() > 1 {
|
||||||
|
let mut len = matched_procs.len();
|
||||||
|
for proc in matched_procs {
|
||||||
|
if proc.root().ends_with("shims") {
|
||||||
|
len -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len > 1 {
|
||||||
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
|
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// File logging worker guard has to have an assignment in the main fn to work
|
|
||||||
let (_guard, _color_guard) = setup()?;
|
|
||||||
|
|
||||||
#[cfg(feature = "deadlock_detection")]
|
|
||||||
detect_deadlocks();
|
|
||||||
|
|
||||||
let process_id = WindowsApi::current_process_id();
|
|
||||||
WindowsApi::allow_set_foreground_window(process_id)?;
|
|
||||||
|
|
||||||
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
|
||||||
crossbeam_channel::unbounded();
|
|
||||||
|
|
||||||
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
|
|
||||||
winevent_listener.start();
|
|
||||||
|
|
||||||
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
|
|
||||||
incoming,
|
|
||||||
)))?));
|
|
||||||
|
|
||||||
wm.lock().init()?;
|
|
||||||
listen_for_commands(wm.clone());
|
|
||||||
listen_for_events(wm.clone());
|
|
||||||
|
|
||||||
load_configuration()?;
|
|
||||||
|
|
||||||
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
|
||||||
ctrlc::set_handler(move || {
|
|
||||||
ctrlc_sender
|
|
||||||
.send(())
|
|
||||||
.expect("could not send signal on ctrl-c channel");
|
|
||||||
})?;
|
|
||||||
|
|
||||||
ctrlc_receiver
|
|
||||||
.recv()
|
|
||||||
.expect("could not receive signal on ctrl-c channel");
|
|
||||||
|
|
||||||
tracing::error!(
|
|
||||||
"received ctrl-c, restoring all hidden windows and terminating process"
|
|
||||||
);
|
|
||||||
|
|
||||||
wm.lock().restore_all_windows();
|
|
||||||
std::process::exit(130);
|
|
||||||
}
|
}
|
||||||
_ => Ok(()),
|
|
||||||
|
// File logging worker guard has to have an assignment in the main fn to work
|
||||||
|
let (_guard, _color_guard) = setup()?;
|
||||||
|
|
||||||
|
#[cfg(feature = "deadlock_detection")]
|
||||||
|
detect_deadlocks();
|
||||||
|
|
||||||
|
let process_id = WindowsApi::current_process_id();
|
||||||
|
WindowsApi::allow_set_foreground_window(process_id)?;
|
||||||
|
WindowsApi::set_process_dpi_awareness_context()?;
|
||||||
|
Border::create("komorebi-border-window")?;
|
||||||
|
|
||||||
|
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
||||||
|
crossbeam_channel::unbounded();
|
||||||
|
|
||||||
|
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
|
||||||
|
winevent_listener.start();
|
||||||
|
|
||||||
|
let wm = Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
|
||||||
|
incoming,
|
||||||
|
)))?));
|
||||||
|
|
||||||
|
wm.lock().init()?;
|
||||||
|
listen_for_commands(wm.clone());
|
||||||
|
std::thread::spawn(|| load_configuration().expect("could not load configuration"));
|
||||||
|
|
||||||
|
if opts.await_configuration {
|
||||||
|
let backoff = Backoff::new();
|
||||||
|
while !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
||||||
|
backoff.snooze();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listen_for_events(wm.clone());
|
||||||
|
|
||||||
|
if CUSTOM_FFM.load(Ordering::SeqCst) {
|
||||||
|
listen_for_movements(wm.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
ctrlc_sender
|
||||||
|
.send(())
|
||||||
|
.expect("could not send signal on ctrl-c channel");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
ctrlc_receiver
|
||||||
|
.recv()
|
||||||
|
.expect("could not receive signal on ctrl-c channel");
|
||||||
|
|
||||||
|
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
|
||||||
|
|
||||||
|
wm.lock().restore_all_windows();
|
||||||
|
|
||||||
|
if WindowsApi::focus_follows_mouse()? {
|
||||||
|
WindowsApi::disable_focus_follows_mouse()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::process::exit(130);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use color_eyre::eyre::ContextCompat;
|
use color_eyre::eyre::anyhow;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use getset::CopyGetters;
|
use getset::CopyGetters;
|
||||||
use getset::Getters;
|
use getset::Getters;
|
||||||
use getset::MutGetters;
|
use getset::MutGetters;
|
||||||
|
use getset::Setters;
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use komorebi_core::Rect;
|
use komorebi_core::Rect;
|
||||||
@@ -14,12 +16,13 @@ use crate::container::Container;
|
|||||||
use crate::ring::Ring;
|
use crate::ring::Ring;
|
||||||
use crate::workspace::Workspace;
|
use crate::workspace::Workspace;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters)]
|
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
|
||||||
pub struct Monitor {
|
pub struct Monitor {
|
||||||
#[getset(get_copy = "pub")]
|
#[getset(get_copy = "pub", set = "pub")]
|
||||||
id: isize,
|
id: isize,
|
||||||
monitor_size: Rect,
|
#[getset(get = "pub", set = "pub")]
|
||||||
#[getset(get = "pub")]
|
size: Rect,
|
||||||
|
#[getset(get = "pub", set = "pub")]
|
||||||
work_area_size: Rect,
|
work_area_size: Rect,
|
||||||
workspaces: Ring<Workspace>,
|
workspaces: Ring<Workspace>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
@@ -29,22 +32,25 @@ pub struct Monitor {
|
|||||||
|
|
||||||
impl_ring_elements!(Monitor, Workspace);
|
impl_ring_elements!(Monitor, Workspace);
|
||||||
|
|
||||||
pub fn new(id: isize, monitor_size: Rect, work_area_size: Rect) -> Monitor {
|
pub fn new(id: isize, size: Rect, work_area_size: Rect) -> Monitor {
|
||||||
|
let mut workspaces = Ring::default();
|
||||||
|
workspaces.elements_mut().push_back(Workspace::default());
|
||||||
|
|
||||||
Monitor {
|
Monitor {
|
||||||
id,
|
id,
|
||||||
monitor_size,
|
size,
|
||||||
work_area_size,
|
work_area_size,
|
||||||
workspaces: Ring::default(),
|
workspaces,
|
||||||
workspace_names: HashMap::default(),
|
workspace_names: HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Monitor {
|
impl Monitor {
|
||||||
pub fn load_focused_workspace(&mut self) -> Result<()> {
|
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
||||||
let focused_idx = self.focused_workspace_idx();
|
let focused_idx = self.focused_workspace_idx();
|
||||||
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
|
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
|
||||||
if i == focused_idx {
|
if i == focused_idx {
|
||||||
workspace.restore()?;
|
workspace.restore(mouse_follows_focus)?;
|
||||||
} else {
|
} else {
|
||||||
workspace.hide();
|
workspace.hide();
|
||||||
}
|
}
|
||||||
@@ -53,16 +59,39 @@ impl Monitor {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_container(&mut self, container: Container) -> Result<()> {
|
pub fn add_container(
|
||||||
let workspace = self
|
&mut self,
|
||||||
.focused_workspace_mut()
|
container: Container,
|
||||||
.context("there is no workspace")?;
|
workspace_idx: Option<usize>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let workspace = if let Some(idx) = workspace_idx {
|
||||||
|
self.workspaces_mut()
|
||||||
|
.get_mut(idx)
|
||||||
|
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
|
||||||
|
} else {
|
||||||
|
self.focused_workspace_mut()
|
||||||
|
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||||
|
};
|
||||||
|
|
||||||
workspace.add_container(container);
|
workspace.add_container(container);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_workspace_by_idx(&mut self, idx: usize) -> Option<Workspace> {
|
||||||
|
if idx < self.workspaces().len() {
|
||||||
|
return self.workspaces_mut().remove(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx == 0 {
|
||||||
|
self.workspaces_mut().push_back(Workspace::default());
|
||||||
|
} else {
|
||||||
|
self.focus_workspace(idx - 1).ok()?;
|
||||||
|
};
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ensure_workspace_count(&mut self, ensure_count: usize) {
|
pub fn ensure_workspace_count(&mut self, ensure_count: usize) {
|
||||||
if self.workspaces().len() < ensure_count {
|
if self.workspaces().len() < ensure_count {
|
||||||
self.workspaces_mut()
|
self.workspaces_mut()
|
||||||
@@ -78,17 +107,17 @@ impl Monitor {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let workspace = self
|
let workspace = self
|
||||||
.focused_workspace_mut()
|
.focused_workspace_mut()
|
||||||
.context("there is no workspace")?;
|
.ok_or_else(|| anyhow!("there is no workspace"))?;
|
||||||
|
|
||||||
if workspace.maximized_window().is_some() {
|
if workspace.maximized_window().is_some() {
|
||||||
return Err(eyre::anyhow!(
|
return Err(anyhow!(
|
||||||
"cannot move native maximized window to another monitor or workspace"
|
"cannot move native maximized window to another monitor or workspace"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let container = workspace
|
let container = workspace
|
||||||
.remove_focused_container()
|
.remove_focused_container()
|
||||||
.context("there is no container")?;
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
|
|
||||||
let workspaces = self.workspaces_mut();
|
let workspaces = self.workspaces_mut();
|
||||||
|
|
||||||
@@ -129,7 +158,7 @@ impl Monitor {
|
|||||||
if name.is_some() {
|
if name.is_some() {
|
||||||
self.workspaces_mut()
|
self.workspaces_mut()
|
||||||
.get_mut(idx)
|
.get_mut(idx)
|
||||||
.context("there is no workspace")?
|
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||||
.set_name(name);
|
.set_name(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,12 +170,16 @@ impl Monitor {
|
|||||||
self.workspaces().len()
|
self.workspaces().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_focused_workspace(&mut self) -> Result<()> {
|
pub fn update_focused_workspace(
|
||||||
|
&mut self,
|
||||||
|
offset: Option<Rect>,
|
||||||
|
invisible_borders: &Rect,
|
||||||
|
) -> Result<()> {
|
||||||
let work_area = *self.work_area_size();
|
let work_area = *self.work_area_size();
|
||||||
|
|
||||||
self.focused_workspace_mut()
|
self.focused_workspace_mut()
|
||||||
.context("there is no workspace")?
|
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||||
.update(&work_area)?;
|
.update(&work_area, offset, invisible_borders)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,54 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use color_eyre::eyre::ContextCompat;
|
use color_eyre::eyre::anyhow;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use miow::pipe::connect;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use schemars::schema_for;
|
||||||
use uds_windows::UnixStream;
|
use uds_windows::UnixStream;
|
||||||
|
|
||||||
use komorebi_core::ApplicationIdentifier;
|
use komorebi_core::ApplicationIdentifier;
|
||||||
|
use komorebi_core::Axis;
|
||||||
|
use komorebi_core::FocusFollowsMouseImplementation;
|
||||||
|
use komorebi_core::Layout;
|
||||||
|
use komorebi_core::MoveBehaviour;
|
||||||
|
use komorebi_core::OperationDirection;
|
||||||
|
use komorebi_core::Rect;
|
||||||
|
use komorebi_core::Sizing;
|
||||||
use komorebi_core::SocketMessage;
|
use komorebi_core::SocketMessage;
|
||||||
|
use komorebi_core::StateQuery;
|
||||||
|
use komorebi_core::WindowContainerBehaviour;
|
||||||
|
|
||||||
|
use crate::border::Border;
|
||||||
|
use crate::current_virtual_desktop;
|
||||||
|
use crate::notify_subscribers;
|
||||||
|
use crate::window::Window;
|
||||||
use crate::window_manager;
|
use crate::window_manager;
|
||||||
use crate::window_manager::WindowManager;
|
use crate::window_manager::WindowManager;
|
||||||
use crate::windows_api::WindowsApi;
|
use crate::windows_api::WindowsApi;
|
||||||
|
use crate::Notification;
|
||||||
|
use crate::NotificationEvent;
|
||||||
|
use crate::BORDER_ENABLED;
|
||||||
|
use crate::BORDER_HWND;
|
||||||
|
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||||
|
use crate::CUSTOM_FFM;
|
||||||
use crate::FLOAT_IDENTIFIERS;
|
use crate::FLOAT_IDENTIFIERS;
|
||||||
|
use crate::HIDING_BEHAVIOUR;
|
||||||
|
use crate::HOME_DIR;
|
||||||
|
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||||
|
use crate::LAYERED_WHITELIST;
|
||||||
use crate::MANAGE_IDENTIFIERS;
|
use crate::MANAGE_IDENTIFIERS;
|
||||||
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
|
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||||
use crate::TRAY_AND_MULTI_WINDOW_EXES;
|
use crate::SUBSCRIPTION_PIPES;
|
||||||
|
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||||
use crate::WORKSPACE_RULES;
|
use crate::WORKSPACE_RULES;
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
@@ -30,7 +59,7 @@ pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
|
|||||||
.try_clone()
|
.try_clone()
|
||||||
.expect("could not clone unix listener");
|
.expect("could not clone unix listener");
|
||||||
|
|
||||||
thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
tracing::info!("listening");
|
tracing::info!("listening");
|
||||||
for client in listener.incoming() {
|
for client in listener.incoming() {
|
||||||
match client {
|
match client {
|
||||||
@@ -50,7 +79,31 @@ pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
|
|||||||
impl WindowManager {
|
impl WindowManager {
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn process_command(&mut self, message: SocketMessage) -> Result<()> {
|
pub fn process_command(&mut self, message: SocketMessage) -> Result<()> {
|
||||||
self.validate_virtual_desktop_id();
|
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
|
||||||
|
if let Some(id) = current_virtual_desktop() {
|
||||||
|
if id != *virtual_desktop_id {
|
||||||
|
tracing::info!(
|
||||||
|
"ignoring events and commands while not on virtual desktop {:?}",
|
||||||
|
virtual_desktop_id
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match message {
|
||||||
|
SocketMessage::CycleFocusMonitor(_)
|
||||||
|
| SocketMessage::CycleFocusWorkspace(_)
|
||||||
|
| SocketMessage::FocusMonitorNumber(_)
|
||||||
|
| SocketMessage::FocusMonitorWorkspaceNumber(_, _)
|
||||||
|
| SocketMessage::FocusWorkspaceNumber(_) => {
|
||||||
|
if self.focused_workspace()?.visible_windows().is_empty() {
|
||||||
|
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||||
|
border.hide()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
SocketMessage::Promote => self.promote_container_to_front()?,
|
SocketMessage::Promote => self.promote_container_to_front()?,
|
||||||
@@ -60,6 +113,12 @@ impl WindowManager {
|
|||||||
SocketMessage::MoveWindow(direction) => {
|
SocketMessage::MoveWindow(direction) => {
|
||||||
self.move_container_in_direction(direction)?;
|
self.move_container_in_direction(direction)?;
|
||||||
}
|
}
|
||||||
|
SocketMessage::CycleFocusWindow(direction) => {
|
||||||
|
self.focus_container_in_cycle_direction(direction)?;
|
||||||
|
}
|
||||||
|
SocketMessage::CycleMoveWindow(direction) => {
|
||||||
|
self.move_container_in_cycle_direction(direction)?;
|
||||||
|
}
|
||||||
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
|
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
|
||||||
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
|
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
|
||||||
SocketMessage::CycleStack(direction) => {
|
SocketMessage::CycleStack(direction) => {
|
||||||
@@ -74,24 +133,71 @@ impl WindowManager {
|
|||||||
SocketMessage::WorkspacePadding(monitor_idx, workspace_idx, size) => {
|
SocketMessage::WorkspacePadding(monitor_idx, workspace_idx, size) => {
|
||||||
self.set_workspace_padding(monitor_idx, workspace_idx, size)?;
|
self.set_workspace_padding(monitor_idx, workspace_idx, size)?;
|
||||||
}
|
}
|
||||||
SocketMessage::WorkspaceRule(_, id, monitor_idx, workspace_idx) => {
|
SocketMessage::WorkspaceRule(_, ref id, monitor_idx, workspace_idx) => {
|
||||||
{
|
{
|
||||||
let mut workspace_rules = WORKSPACE_RULES.lock();
|
let mut workspace_rules = WORKSPACE_RULES.lock();
|
||||||
workspace_rules.insert(id, (monitor_idx, workspace_idx));
|
workspace_rules.insert(id.to_string(), (monitor_idx, workspace_idx));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.enforce_workspace_rules()?;
|
self.enforce_workspace_rules()?;
|
||||||
}
|
}
|
||||||
SocketMessage::ManageRule(_, id) => {
|
SocketMessage::ManageRule(_, ref id) => {
|
||||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||||
if !manage_identifiers.contains(&id) {
|
if !manage_identifiers.contains(id) {
|
||||||
manage_identifiers.push(id);
|
manage_identifiers.push(id.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SocketMessage::FloatRule(_, id) => {
|
SocketMessage::FloatRule(identifier, ref id) => {
|
||||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||||
if !float_identifiers.contains(&id) {
|
if !float_identifiers.contains(id) {
|
||||||
float_identifiers.push(id);
|
float_identifiers.push(id.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let invisible_borders = self.invisible_borders;
|
||||||
|
let offset = self.work_area_offset;
|
||||||
|
|
||||||
|
let mut hwnds_to_purge = vec![];
|
||||||
|
for (i, monitor) in self.monitors().iter().enumerate() {
|
||||||
|
for container in monitor
|
||||||
|
.focused_workspace()
|
||||||
|
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||||
|
.containers()
|
||||||
|
.iter()
|
||||||
|
{
|
||||||
|
for window in container.windows().iter() {
|
||||||
|
match identifier {
|
||||||
|
ApplicationIdentifier::Exe => {
|
||||||
|
if window.exe()? == *id {
|
||||||
|
hwnds_to_purge.push((i, window.hwnd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApplicationIdentifier::Class => {
|
||||||
|
if window.class()? == *id {
|
||||||
|
hwnds_to_purge.push((i, window.hwnd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApplicationIdentifier::Title => {
|
||||||
|
if window.title()? == *id {
|
||||||
|
hwnds_to_purge.push((i, window.hwnd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (monitor_idx, hwnd) in hwnds_to_purge {
|
||||||
|
let monitor = self
|
||||||
|
.monitors_mut()
|
||||||
|
.get_mut(monitor_idx)
|
||||||
|
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||||
|
|
||||||
|
monitor
|
||||||
|
.focused_workspace_mut()
|
||||||
|
.ok_or_else(|| anyhow!("there is no focused workspace"))?
|
||||||
|
.remove_window(hwnd)?;
|
||||||
|
|
||||||
|
monitor.update_focused_workspace(offset, &invisible_borders)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SocketMessage::AdjustContainerPadding(sizing, adjustment) => {
|
SocketMessage::AdjustContainerPadding(sizing, adjustment) => {
|
||||||
@@ -104,43 +210,129 @@ impl WindowManager {
|
|||||||
self.move_container_to_workspace(workspace_idx, true)?;
|
self.move_container_to_workspace(workspace_idx, true)?;
|
||||||
}
|
}
|
||||||
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
|
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
|
||||||
self.move_container_to_monitor(monitor_idx, true)?;
|
self.move_container_to_monitor(monitor_idx, None, true)?;
|
||||||
|
}
|
||||||
|
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
|
||||||
|
self.move_container_to_workspace(workspace_idx, false)?;
|
||||||
|
}
|
||||||
|
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
|
||||||
|
self.move_container_to_monitor(monitor_idx, None, false)?;
|
||||||
|
}
|
||||||
|
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
||||||
|
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
|
||||||
|
}
|
||||||
|
SocketMessage::MoveWorkspaceToMonitorNumber(monitor_idx) => {
|
||||||
|
self.move_workspace_to_monitor(monitor_idx)?;
|
||||||
}
|
}
|
||||||
SocketMessage::TogglePause => {
|
SocketMessage::TogglePause => {
|
||||||
tracing::info!("pausing");
|
if self.is_paused {
|
||||||
|
tracing::info!("resuming");
|
||||||
|
} else {
|
||||||
|
tracing::info!("pausing");
|
||||||
|
}
|
||||||
|
|
||||||
self.is_paused = !self.is_paused;
|
self.is_paused = !self.is_paused;
|
||||||
|
self.retile_all(true)?;
|
||||||
}
|
}
|
||||||
SocketMessage::ToggleTiling => {
|
SocketMessage::ToggleTiling => {
|
||||||
self.toggle_tiling()?;
|
self.toggle_tiling()?;
|
||||||
}
|
}
|
||||||
|
SocketMessage::CycleFocusMonitor(direction) => {
|
||||||
|
let monitor_idx = direction.next_idx(
|
||||||
|
self.focused_monitor_idx(),
|
||||||
|
NonZeroUsize::new(self.monitors().len())
|
||||||
|
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.focus_monitor(monitor_idx)?;
|
||||||
|
self.update_focused_workspace(self.mouse_follows_focus)?;
|
||||||
|
}
|
||||||
SocketMessage::FocusMonitorNumber(monitor_idx) => {
|
SocketMessage::FocusMonitorNumber(monitor_idx) => {
|
||||||
self.focus_monitor(monitor_idx)?;
|
self.focus_monitor(monitor_idx)?;
|
||||||
self.update_focused_workspace(true)?;
|
self.update_focused_workspace(self.mouse_follows_focus)?;
|
||||||
}
|
|
||||||
SocketMessage::Retile => {
|
|
||||||
for monitor in self.monitors_mut() {
|
|
||||||
let work_area = *monitor.work_area_size();
|
|
||||||
let workspace = monitor
|
|
||||||
.focused_workspace_mut()
|
|
||||||
.context("there is no workspace")?;
|
|
||||||
|
|
||||||
// Reset any resize adjustments if we want to force a retile
|
|
||||||
for resize in workspace.resize_dimensions_mut() {
|
|
||||||
*resize = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace.update(&work_area)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
SocketMessage::Retile => self.retile_all(false)?,
|
||||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
||||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout(layout)?,
|
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||||
|
SocketMessage::ChangeLayoutCustom(ref path) => {
|
||||||
|
self.change_workspace_custom_layout(path.clone())?;
|
||||||
|
}
|
||||||
|
SocketMessage::WorkspaceLayoutCustom(monitor_idx, workspace_idx, ref path) => {
|
||||||
|
self.set_workspace_layout_custom(monitor_idx, workspace_idx, path.clone())?;
|
||||||
|
}
|
||||||
SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => {
|
SocketMessage::WorkspaceTiling(monitor_idx, workspace_idx, tile) => {
|
||||||
self.set_workspace_tiling(monitor_idx, workspace_idx, tile)?;
|
self.set_workspace_tiling(monitor_idx, workspace_idx, tile)?;
|
||||||
}
|
}
|
||||||
SocketMessage::WorkspaceLayout(monitor_idx, workspace_idx, layout) => {
|
SocketMessage::WorkspaceLayout(monitor_idx, workspace_idx, layout) => {
|
||||||
self.set_workspace_layout(monitor_idx, workspace_idx, layout)?;
|
self.set_workspace_layout_default(monitor_idx, workspace_idx, layout)?;
|
||||||
|
}
|
||||||
|
SocketMessage::WorkspaceLayoutRule(
|
||||||
|
monitor_idx,
|
||||||
|
workspace_idx,
|
||||||
|
at_container_count,
|
||||||
|
layout,
|
||||||
|
) => {
|
||||||
|
self.add_workspace_layout_default_rule(
|
||||||
|
monitor_idx,
|
||||||
|
workspace_idx,
|
||||||
|
at_container_count,
|
||||||
|
layout,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
SocketMessage::WorkspaceLayoutCustomRule(
|
||||||
|
monitor_idx,
|
||||||
|
workspace_idx,
|
||||||
|
at_container_count,
|
||||||
|
ref path,
|
||||||
|
) => {
|
||||||
|
self.add_workspace_layout_custom_rule(
|
||||||
|
monitor_idx,
|
||||||
|
workspace_idx,
|
||||||
|
at_container_count,
|
||||||
|
path.clone(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
SocketMessage::ClearWorkspaceLayoutRules(monitor_idx, workspace_idx) => {
|
||||||
|
self.clear_workspace_layout_rules(monitor_idx, workspace_idx)?;
|
||||||
|
}
|
||||||
|
SocketMessage::CycleFocusWorkspace(direction) => {
|
||||||
|
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||||
|
// secondary monitor where the cursor is focused will be used as the target for
|
||||||
|
// the workspace switch op
|
||||||
|
let monitor_idx = self.monitor_idx_from_current_pos().ok_or_else(|| {
|
||||||
|
anyhow!("there is no monitor associated with the current cursor position")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.focus_monitor(monitor_idx)?;
|
||||||
|
|
||||||
|
let focused_monitor = self
|
||||||
|
.focused_monitor()
|
||||||
|
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||||
|
|
||||||
|
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
|
||||||
|
let workspaces = focused_monitor.workspaces().len();
|
||||||
|
|
||||||
|
let workspace_idx = direction.next_idx(
|
||||||
|
focused_workspace_idx,
|
||||||
|
NonZeroUsize::new(workspaces)
|
||||||
|
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.focus_workspace(workspace_idx)?;
|
||||||
}
|
}
|
||||||
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
|
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
|
||||||
|
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||||
|
// secondary monitor where the cursor is focused will be used as the target for
|
||||||
|
// the workspace switch op
|
||||||
|
let monitor_idx = self.monitor_idx_from_current_pos().ok_or_else(|| {
|
||||||
|
anyhow!("there is no monitor associated with the current cursor position")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.focus_monitor(monitor_idx)?;
|
||||||
|
self.focus_workspace(workspace_idx)?;
|
||||||
|
}
|
||||||
|
SocketMessage::FocusMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
||||||
|
self.focus_monitor(monitor_idx)?;
|
||||||
self.focus_workspace(workspace_idx)?;
|
self.focus_workspace(workspace_idx)?;
|
||||||
}
|
}
|
||||||
SocketMessage::Stop => {
|
SocketMessage::Stop => {
|
||||||
@@ -148,6 +340,11 @@ impl WindowManager {
|
|||||||
"received stop command, restoring all hidden windows and terminating process"
|
"received stop command, restoring all hidden windows and terminating process"
|
||||||
);
|
);
|
||||||
self.restore_all_windows();
|
self.restore_all_windows();
|
||||||
|
|
||||||
|
if WindowsApi::focus_follows_mouse()? {
|
||||||
|
WindowsApi::disable_focus_follows_mouse()?;
|
||||||
|
}
|
||||||
|
|
||||||
std::process::exit(0)
|
std::process::exit(0)
|
||||||
}
|
}
|
||||||
SocketMessage::EnsureWorkspaces(monitor_idx, workspace_count) => {
|
SocketMessage::EnsureWorkspaces(monitor_idx, workspace_count) => {
|
||||||
@@ -156,56 +353,478 @@ impl WindowManager {
|
|||||||
SocketMessage::NewWorkspace => {
|
SocketMessage::NewWorkspace => {
|
||||||
self.new_workspace()?;
|
self.new_workspace()?;
|
||||||
}
|
}
|
||||||
SocketMessage::WorkspaceName(monitor_idx, workspace_idx, name) => {
|
SocketMessage::WorkspaceName(monitor_idx, workspace_idx, ref name) => {
|
||||||
self.set_workspace_name(monitor_idx, workspace_idx, name)?;
|
self.set_workspace_name(monitor_idx, workspace_idx, name.to_string())?;
|
||||||
}
|
}
|
||||||
SocketMessage::State => {
|
SocketMessage::State => {
|
||||||
let state = serde_json::to_string_pretty(&window_manager::State::from(self))?;
|
let state = match serde_json::to_string_pretty(&window_manager::State::from(&*self))
|
||||||
let mut socket = dirs::home_dir().context("there is no home directory")?;
|
{
|
||||||
|
Ok(state) => state,
|
||||||
|
Err(error) => error.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut socket = HOME_DIR.clone();
|
||||||
socket.push("komorebic.sock");
|
socket.push("komorebic.sock");
|
||||||
let socket = socket.as_path();
|
let socket = socket.as_path();
|
||||||
|
|
||||||
let mut stream = UnixStream::connect(&socket)?;
|
let mut stream = UnixStream::connect(&socket)?;
|
||||||
stream.write_all(state.as_bytes())?;
|
stream.write_all(state.as_bytes())?;
|
||||||
}
|
}
|
||||||
SocketMessage::ResizeWindow(direction, sizing) => {
|
SocketMessage::Query(query) => {
|
||||||
self.resize_window(direction, sizing, Option::from(50))?;
|
let response = match query {
|
||||||
|
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx(),
|
||||||
|
StateQuery::FocusedWorkspaceIndex => self
|
||||||
|
.focused_monitor()
|
||||||
|
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||||
|
.focused_workspace_idx(),
|
||||||
|
StateQuery::FocusedContainerIndex => {
|
||||||
|
self.focused_workspace()?.focused_container_idx()
|
||||||
|
}
|
||||||
|
StateQuery::FocusedWindowIndex => {
|
||||||
|
self.focused_container()?.focused_window_idx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut socket = HOME_DIR.clone();
|
||||||
|
socket.push("komorebic.sock");
|
||||||
|
let socket = socket.as_path();
|
||||||
|
|
||||||
|
let mut stream = UnixStream::connect(&socket)?;
|
||||||
|
stream.write_all(response.as_bytes())?;
|
||||||
}
|
}
|
||||||
SocketMessage::FocusFollowsMouse(enable) => {
|
SocketMessage::ResizeWindowEdge(direction, sizing) => {
|
||||||
if enable {
|
self.resize_window(direction, sizing, self.resize_delta, true)?;
|
||||||
WindowsApi::enable_focus_follows_mouse()?;
|
}
|
||||||
|
SocketMessage::ResizeWindowAxis(axis, sizing) => {
|
||||||
|
// If the user has a custom layout, allow for the resizing of the primary column
|
||||||
|
// with this signal
|
||||||
|
let workspace = self.focused_workspace_mut()?;
|
||||||
|
let container_len = workspace.containers().len();
|
||||||
|
let no_layout_rules = workspace.layout_rules().is_empty();
|
||||||
|
|
||||||
|
if let Layout::Custom(ref mut custom) = workspace.layout_mut() {
|
||||||
|
if matches!(axis, Axis::Horizontal) {
|
||||||
|
let percentage = custom
|
||||||
|
.primary_width_percentage()
|
||||||
|
.unwrap_or(100 / custom.len());
|
||||||
|
|
||||||
|
if no_layout_rules {
|
||||||
|
match sizing {
|
||||||
|
Sizing::Increase => {
|
||||||
|
custom.set_primary_width_percentage(percentage + 5);
|
||||||
|
}
|
||||||
|
Sizing::Decrease => {
|
||||||
|
custom.set_primary_width_percentage(percentage - 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for rule in workspace.layout_rules_mut() {
|
||||||
|
if container_len >= rule.0 {
|
||||||
|
if let Layout::Custom(ref mut custom) = rule.1 {
|
||||||
|
match sizing {
|
||||||
|
Sizing::Increase => {
|
||||||
|
custom.set_primary_width_percentage(percentage + 5);
|
||||||
|
}
|
||||||
|
Sizing::Decrease => {
|
||||||
|
custom.set_primary_width_percentage(percentage - 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise proceed with the resizing logic for individual window containers in the
|
||||||
|
// assumed BSP layout
|
||||||
} else {
|
} else {
|
||||||
WindowsApi::disable_focus_follows_mouse()?;
|
match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
self.resize_window(
|
||||||
|
OperationDirection::Left,
|
||||||
|
sizing,
|
||||||
|
self.resize_delta,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
self.resize_window(
|
||||||
|
OperationDirection::Right,
|
||||||
|
sizing,
|
||||||
|
self.resize_delta,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Axis::Vertical => {
|
||||||
|
self.resize_window(
|
||||||
|
OperationDirection::Up,
|
||||||
|
sizing,
|
||||||
|
self.resize_delta,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
self.resize_window(
|
||||||
|
OperationDirection::Down,
|
||||||
|
sizing,
|
||||||
|
self.resize_delta,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Axis::HorizontalAndVertical => {
|
||||||
|
self.resize_window(
|
||||||
|
OperationDirection::Left,
|
||||||
|
sizing,
|
||||||
|
self.resize_delta,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
self.resize_window(
|
||||||
|
OperationDirection::Right,
|
||||||
|
sizing,
|
||||||
|
self.resize_delta,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
self.resize_window(
|
||||||
|
OperationDirection::Up,
|
||||||
|
sizing,
|
||||||
|
self.resize_delta,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
self.resize_window(
|
||||||
|
OperationDirection::Down,
|
||||||
|
sizing,
|
||||||
|
self.resize_delta,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_focused_workspace(false)?;
|
||||||
|
}
|
||||||
|
SocketMessage::FocusFollowsMouse(mut implementation, enable) => {
|
||||||
|
if !CUSTOM_FFM.load(Ordering::SeqCst) {
|
||||||
|
tracing::warn!(
|
||||||
|
"komorebi was not started with the --ffm flag, so the komorebi implementation of focus follows mouse cannot be enabled; defaulting to windows implementation"
|
||||||
|
);
|
||||||
|
implementation = FocusFollowsMouseImplementation::Windows;
|
||||||
|
}
|
||||||
|
|
||||||
|
match implementation {
|
||||||
|
FocusFollowsMouseImplementation::Komorebi => {
|
||||||
|
if WindowsApi::focus_follows_mouse()? {
|
||||||
|
tracing::warn!(
|
||||||
|
"the komorebi implementation of focus follows mouse cannot be enabled while the windows implementation is enabled"
|
||||||
|
);
|
||||||
|
} else if enable {
|
||||||
|
self.focus_follows_mouse = Option::from(implementation);
|
||||||
|
} else {
|
||||||
|
self.focus_follows_mouse = None;
|
||||||
|
self.has_pending_raise_op = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FocusFollowsMouseImplementation::Windows => {
|
||||||
|
if let Some(FocusFollowsMouseImplementation::Komorebi) =
|
||||||
|
self.focus_follows_mouse
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"the windows implementation of focus follows mouse cannot be enabled while the komorebi implementation is enabled"
|
||||||
|
);
|
||||||
|
} else if enable {
|
||||||
|
WindowsApi::enable_focus_follows_mouse()?;
|
||||||
|
self.focus_follows_mouse =
|
||||||
|
Option::from(FocusFollowsMouseImplementation::Windows);
|
||||||
|
} else {
|
||||||
|
WindowsApi::disable_focus_follows_mouse()?;
|
||||||
|
self.focus_follows_mouse = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SocketMessage::ToggleFocusFollowsMouse(mut implementation) => {
|
||||||
|
if !CUSTOM_FFM.load(Ordering::SeqCst) {
|
||||||
|
tracing::warn!(
|
||||||
|
"komorebi was not started with the --ffm flag, so the komorebi implementation of focus follows mouse cannot be toggled; defaulting to windows implementation"
|
||||||
|
);
|
||||||
|
implementation = FocusFollowsMouseImplementation::Windows;
|
||||||
|
}
|
||||||
|
|
||||||
|
match implementation {
|
||||||
|
FocusFollowsMouseImplementation::Komorebi => {
|
||||||
|
if WindowsApi::focus_follows_mouse()? {
|
||||||
|
tracing::warn!(
|
||||||
|
"the komorebi implementation of focus follows mouse cannot be toggled while the windows implementation is enabled"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
match self.focus_follows_mouse {
|
||||||
|
None => {
|
||||||
|
self.focus_follows_mouse = Option::from(implementation);
|
||||||
|
self.has_pending_raise_op = false;
|
||||||
|
}
|
||||||
|
Some(FocusFollowsMouseImplementation::Komorebi) => {
|
||||||
|
self.focus_follows_mouse = None;
|
||||||
|
}
|
||||||
|
Some(FocusFollowsMouseImplementation::Windows) => {
|
||||||
|
tracing::warn!("ignoring command that could mix different focus follows mouse implementations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FocusFollowsMouseImplementation::Windows => {
|
||||||
|
if let Some(FocusFollowsMouseImplementation::Komorebi) =
|
||||||
|
self.focus_follows_mouse
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"the windows implementation of focus follows mouse cannot be toggled while the komorebi implementation is enabled"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
match self.focus_follows_mouse {
|
||||||
|
None => {
|
||||||
|
WindowsApi::enable_focus_follows_mouse()?;
|
||||||
|
self.focus_follows_mouse = Option::from(implementation);
|
||||||
|
}
|
||||||
|
Some(FocusFollowsMouseImplementation::Windows) => {
|
||||||
|
WindowsApi::disable_focus_follows_mouse()?;
|
||||||
|
self.focus_follows_mouse = None;
|
||||||
|
}
|
||||||
|
Some(FocusFollowsMouseImplementation::Komorebi) => {
|
||||||
|
tracing::warn!("ignoring command that could mix different focus follows mouse implementations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SocketMessage::ReloadConfiguration => {
|
SocketMessage::ReloadConfiguration => {
|
||||||
Self::reload_configuration();
|
Self::reload_configuration();
|
||||||
}
|
}
|
||||||
|
SocketMessage::CompleteConfiguration => {
|
||||||
|
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
||||||
|
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
|
||||||
|
self.update_focused_workspace(false)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
SocketMessage::WatchConfiguration(enable) => {
|
SocketMessage::WatchConfiguration(enable) => {
|
||||||
self.watch_configuration(enable)?;
|
self.watch_configuration(enable)?;
|
||||||
}
|
}
|
||||||
SocketMessage::IdentifyTrayApplication(identifier, id) => match identifier {
|
SocketMessage::IdentifyBorderOverflowApplication(_, ref id) => {
|
||||||
ApplicationIdentifier::Exe => {
|
let mut identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||||
let mut exes = TRAY_AND_MULTI_WINDOW_EXES.lock();
|
if !identifiers.contains(id) {
|
||||||
if !exes.contains(&id) {
|
identifiers.push(id.to_string());
|
||||||
exes.push(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ApplicationIdentifier::Class => {
|
}
|
||||||
let mut classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock();
|
SocketMessage::IdentifyObjectNameChangeApplication(_, ref id) => {
|
||||||
if !classes.contains(&id) {
|
let mut identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||||
classes.push(id);
|
if !identifiers.contains(id) {
|
||||||
}
|
identifiers.push(id.to_string());
|
||||||
}
|
}
|
||||||
ApplicationIdentifier::Title => {}
|
}
|
||||||
},
|
SocketMessage::IdentifyTrayApplication(_, ref id) => {
|
||||||
|
let mut identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||||
|
if !identifiers.contains(id) {
|
||||||
|
identifiers.push(id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SocketMessage::IdentifyLayeredApplication(_, ref id) => {
|
||||||
|
let mut identifiers = LAYERED_WHITELIST.lock();
|
||||||
|
if !identifiers.contains(id) {
|
||||||
|
identifiers.push(id.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
SocketMessage::ManageFocusedWindow => {
|
SocketMessage::ManageFocusedWindow => {
|
||||||
self.manage_focused_window()?;
|
self.manage_focused_window()?;
|
||||||
}
|
}
|
||||||
SocketMessage::UnmanageFocusedWindow => {
|
SocketMessage::UnmanageFocusedWindow => {
|
||||||
self.unmanage_focused_window()?;
|
self.unmanage_focused_window()?;
|
||||||
}
|
}
|
||||||
}
|
SocketMessage::InvisibleBorders(rect) => {
|
||||||
|
self.invisible_borders = rect;
|
||||||
|
self.retile_all(false)?;
|
||||||
|
}
|
||||||
|
SocketMessage::WorkAreaOffset(rect) => {
|
||||||
|
self.work_area_offset = Option::from(rect);
|
||||||
|
self.retile_all(false)?;
|
||||||
|
}
|
||||||
|
SocketMessage::QuickSave => {
|
||||||
|
let workspace = self.focused_workspace()?;
|
||||||
|
let resize = workspace.resize_dimensions();
|
||||||
|
|
||||||
|
let mut quicksave_json = std::env::temp_dir();
|
||||||
|
quicksave_json.push("komorebi.quicksave.json");
|
||||||
|
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.create(true)
|
||||||
|
.open(quicksave_json)?;
|
||||||
|
|
||||||
|
serde_json::to_writer_pretty(&file, &resize)?;
|
||||||
|
}
|
||||||
|
SocketMessage::QuickLoad => {
|
||||||
|
let workspace = self.focused_workspace_mut()?;
|
||||||
|
|
||||||
|
let mut quicksave_json = std::env::temp_dir();
|
||||||
|
quicksave_json.push("komorebi.quicksave.json");
|
||||||
|
|
||||||
|
let file = File::open(&quicksave_json).map_err(|_| {
|
||||||
|
anyhow!(
|
||||||
|
"no quicksave found at {}",
|
||||||
|
quicksave_json.display().to_string()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
||||||
|
|
||||||
|
workspace.set_resize_dimensions(resize);
|
||||||
|
self.update_focused_workspace(false)?;
|
||||||
|
}
|
||||||
|
SocketMessage::Save(ref path) => {
|
||||||
|
let workspace = self.focused_workspace_mut()?;
|
||||||
|
let resize = workspace.resize_dimensions();
|
||||||
|
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.create(true)
|
||||||
|
.open(path.clone())?;
|
||||||
|
|
||||||
|
serde_json::to_writer_pretty(&file, &resize)?;
|
||||||
|
}
|
||||||
|
SocketMessage::Load(ref path) => {
|
||||||
|
let workspace = self.focused_workspace_mut()?;
|
||||||
|
|
||||||
|
let file = File::open(path)
|
||||||
|
.map_err(|_| anyhow!("no file found at {}", path.display().to_string()))?;
|
||||||
|
|
||||||
|
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
||||||
|
|
||||||
|
workspace.set_resize_dimensions(resize);
|
||||||
|
self.update_focused_workspace(false)?;
|
||||||
|
}
|
||||||
|
SocketMessage::AddSubscriber(ref subscriber) => {
|
||||||
|
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||||
|
let pipe_path = format!(r"\\.\pipe\{}", subscriber);
|
||||||
|
let pipe = connect(&pipe_path).map_err(|_| {
|
||||||
|
anyhow!("the named pipe '{}' has not yet been created; please create it before running this command", pipe_path)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
pipes.insert(subscriber.clone(), pipe);
|
||||||
|
}
|
||||||
|
SocketMessage::RemoveSubscriber(ref subscriber) => {
|
||||||
|
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||||
|
pipes.remove(subscriber);
|
||||||
|
}
|
||||||
|
SocketMessage::MouseFollowsFocus(enable) => {
|
||||||
|
self.mouse_follows_focus = enable;
|
||||||
|
}
|
||||||
|
SocketMessage::ToggleMouseFollowsFocus => {
|
||||||
|
self.mouse_follows_focus = !self.mouse_follows_focus;
|
||||||
|
}
|
||||||
|
SocketMessage::ResizeDelta(delta) => {
|
||||||
|
self.resize_delta = delta;
|
||||||
|
}
|
||||||
|
SocketMessage::ToggleWindowContainerBehaviour => {
|
||||||
|
match self.window_container_behaviour {
|
||||||
|
WindowContainerBehaviour::Create => {
|
||||||
|
self.window_container_behaviour = WindowContainerBehaviour::Append;
|
||||||
|
}
|
||||||
|
WindowContainerBehaviour::Append => {
|
||||||
|
self.window_container_behaviour = WindowContainerBehaviour::Create;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SocketMessage::WindowHidingBehaviour(behaviour) => {
|
||||||
|
let mut hiding_behaviour = HIDING_BEHAVIOUR.lock();
|
||||||
|
*hiding_behaviour = behaviour;
|
||||||
|
}
|
||||||
|
SocketMessage::ToggleCrossMonitorMoveBehaviour => {
|
||||||
|
match self.cross_monitor_move_behaviour {
|
||||||
|
MoveBehaviour::Swap => {
|
||||||
|
self.cross_monitor_move_behaviour = MoveBehaviour::Insert;
|
||||||
|
}
|
||||||
|
MoveBehaviour::Insert => {
|
||||||
|
self.cross_monitor_move_behaviour = MoveBehaviour::Swap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SocketMessage::CrossMonitorMoveBehaviour(behaviour) => {
|
||||||
|
self.cross_monitor_move_behaviour = behaviour;
|
||||||
|
}
|
||||||
|
SocketMessage::UnmanagedWindowOperationBehaviour(behaviour) => {
|
||||||
|
self.unmanaged_window_operation_behaviour = behaviour;
|
||||||
|
}
|
||||||
|
SocketMessage::ActiveWindowBorder(enable) => {
|
||||||
|
if enable {
|
||||||
|
BORDER_ENABLED.store(true, Ordering::SeqCst);
|
||||||
|
self.show_border()?;
|
||||||
|
} else {
|
||||||
|
BORDER_ENABLED.store(false, Ordering::SeqCst);
|
||||||
|
self.hide_border()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SocketMessage::ActiveWindowBorderColour(r, g, b) => {
|
||||||
|
let hwnd = BORDER_HWND.load(Ordering::SeqCst);
|
||||||
|
WindowsApi::change_border_colour(hwnd, r, g, b)?;
|
||||||
|
}
|
||||||
|
SocketMessage::NotificationSchema => {
|
||||||
|
let notification = schema_for!(Notification);
|
||||||
|
let schema = serde_json::to_string_pretty(¬ification)?;
|
||||||
|
let mut socket = HOME_DIR.clone();
|
||||||
|
socket.push("komorebic.sock");
|
||||||
|
let socket = socket.as_path();
|
||||||
|
|
||||||
|
let mut stream = UnixStream::connect(&socket)?;
|
||||||
|
stream.write_all(schema.as_bytes())?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match message {
|
||||||
|
SocketMessage::ChangeLayout(_)
|
||||||
|
| SocketMessage::ChangeLayoutCustom(_)
|
||||||
|
| SocketMessage::FlipLayout(_)
|
||||||
|
| SocketMessage::ManageFocusedWindow
|
||||||
|
| SocketMessage::MoveWorkspaceToMonitorNumber(_)
|
||||||
|
| SocketMessage::MoveContainerToMonitorNumber(_)
|
||||||
|
| SocketMessage::MoveContainerToWorkspaceNumber(_)
|
||||||
|
| SocketMessage::ResizeWindowEdge(_, _)
|
||||||
|
| SocketMessage::ResizeWindowAxis(_, _)
|
||||||
|
| SocketMessage::ToggleFloat
|
||||||
|
| SocketMessage::ToggleMonocle
|
||||||
|
| SocketMessage::ToggleMaximize
|
||||||
|
| SocketMessage::Promote
|
||||||
|
| SocketMessage::Retile
|
||||||
|
| SocketMessage::MoveWindow(_) => {
|
||||||
|
let foreground = WindowsApi::foreground_window()?;
|
||||||
|
let foreground_window = Window { hwnd: foreground };
|
||||||
|
let mut rect = WindowsApi::window_rect(foreground_window.hwnd())?;
|
||||||
|
rect.top -= self.invisible_borders.bottom;
|
||||||
|
rect.bottom += self.invisible_borders.bottom;
|
||||||
|
|
||||||
|
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||||
|
border.set_position(foreground_window, &self.invisible_borders, false)?;
|
||||||
|
}
|
||||||
|
SocketMessage::TogglePause => {
|
||||||
|
let is_paused = self.is_paused;
|
||||||
|
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||||
|
|
||||||
|
if is_paused {
|
||||||
|
border.hide()?;
|
||||||
|
} else {
|
||||||
|
let focused = self.focused_window()?;
|
||||||
|
border.set_position(*focused, &self.invisible_borders, true)?;
|
||||||
|
focused.focus(false)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SocketMessage::ToggleTiling | SocketMessage::WorkspaceTiling(..) => {
|
||||||
|
let tiling_enabled = *self.focused_workspace_mut()?.tile();
|
||||||
|
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||||
|
|
||||||
|
if tiling_enabled {
|
||||||
|
let focused = self.focused_window()?;
|
||||||
|
border.set_position(*focused, &self.invisible_borders, true)?;
|
||||||
|
focused.focus(false)?;
|
||||||
|
} else {
|
||||||
|
border.hide()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
tracing::info!("processed");
|
tracing::info!("processed");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -218,17 +837,22 @@ impl WindowManager {
|
|||||||
let message = SocketMessage::from_str(&line?)?;
|
let message = SocketMessage::from_str(&line?)?;
|
||||||
|
|
||||||
if self.is_paused {
|
if self.is_paused {
|
||||||
if let SocketMessage::TogglePause = message {
|
return match message {
|
||||||
tracing::info!("resuming");
|
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
|
||||||
self.is_paused = !self.is_paused;
|
Ok(self.process_command(message)?)
|
||||||
return Ok(());
|
}
|
||||||
}
|
_ => {
|
||||||
|
tracing::trace!("ignoring while paused");
|
||||||
tracing::trace!("ignoring while paused");
|
Ok(())
|
||||||
return Ok(());
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
self.process_command(message)?;
|
self.process_command(message.clone())?;
|
||||||
|
notify_subscribers(&serde_json::to_string(&Notification {
|
||||||
|
event: NotificationEvent::Socket(message.clone()),
|
||||||
|
state: self.as_ref().into(),
|
||||||
|
})?)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use color_eyre::eyre::ContextCompat;
|
use color_eyre::eyre::anyhow;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use crossbeam_channel::select;
|
use crossbeam_channel::select;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@@ -10,19 +10,28 @@ use parking_lot::Mutex;
|
|||||||
use komorebi_core::OperationDirection;
|
use komorebi_core::OperationDirection;
|
||||||
use komorebi_core::Rect;
|
use komorebi_core::Rect;
|
||||||
use komorebi_core::Sizing;
|
use komorebi_core::Sizing;
|
||||||
|
use komorebi_core::WindowContainerBehaviour;
|
||||||
|
|
||||||
|
use crate::border::Border;
|
||||||
|
use crate::current_virtual_desktop;
|
||||||
|
use crate::notify_subscribers;
|
||||||
use crate::window_manager::WindowManager;
|
use crate::window_manager::WindowManager;
|
||||||
use crate::window_manager_event::WindowManagerEvent;
|
use crate::window_manager_event::WindowManagerEvent;
|
||||||
use crate::windows_api::WindowsApi;
|
use crate::windows_api::WindowsApi;
|
||||||
|
use crate::Notification;
|
||||||
|
use crate::NotificationEvent;
|
||||||
|
use crate::BORDER_ENABLED;
|
||||||
|
use crate::BORDER_HIDDEN;
|
||||||
|
use crate::BORDER_HWND;
|
||||||
|
use crate::DATA_DIR;
|
||||||
use crate::HIDDEN_HWNDS;
|
use crate::HIDDEN_HWNDS;
|
||||||
use crate::TRAY_AND_MULTI_WINDOW_CLASSES;
|
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||||
use crate::TRAY_AND_MULTI_WINDOW_EXES;
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||||
let receiver = wm.lock().incoming_events.lock().clone();
|
let receiver = wm.lock().incoming_events.lock().clone();
|
||||||
|
|
||||||
thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
tracing::info!("listening");
|
tracing::info!("listening");
|
||||||
loop {
|
loop {
|
||||||
select! {
|
select! {
|
||||||
@@ -48,28 +57,59 @@ impl WindowManager {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.validate_virtual_desktop_id();
|
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
|
||||||
|
if let Some(id) = current_virtual_desktop() {
|
||||||
|
if id != *virtual_desktop_id {
|
||||||
|
tracing::info!(
|
||||||
|
"ignoring events and commands while not on virtual desktop {:?}",
|
||||||
|
virtual_desktop_id
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure we have the most recently focused monitor from any event
|
// Make sure we have the most recently focused monitor from any event
|
||||||
match event {
|
match event {
|
||||||
WindowManagerEvent::FocusChange(_, window)
|
WindowManagerEvent::MonitorPoll(_, window)
|
||||||
|
| WindowManagerEvent::FocusChange(_, window)
|
||||||
| WindowManagerEvent::Show(_, window)
|
| WindowManagerEvent::Show(_, window)
|
||||||
| WindowManagerEvent::MoveResizeEnd(_, window) => {
|
| WindowManagerEvent::MoveResizeEnd(_, window) => {
|
||||||
let monitor_idx = self
|
self.reconcile_monitors()?;
|
||||||
.monitor_idx_from_window(*window)
|
|
||||||
.context("there is no monitor associated with this window, it may have already been destroyed")?;
|
|
||||||
|
|
||||||
self.focus_monitor(monitor_idx)?;
|
let monitor_idx = self.monitor_idx_from_window(*window)
|
||||||
|
.ok_or_else(|| anyhow!("there is no monitor associated with this window, it may have already been destroyed"))?;
|
||||||
|
|
||||||
|
// This is a hidden window apparently associated with COM support mechanisms (based
|
||||||
|
// on a post from http://www.databaseteam.org/1-ms-sql-server/a5bb344836fb889c.htm)
|
||||||
|
//
|
||||||
|
// The hidden window, OLEChannelWnd, associated with this class (spawned by
|
||||||
|
// explorer.exe), after some debugging, is observed to always be tied to the primary
|
||||||
|
// display monitor, or (usually) monitor 0 in the WindowManager state.
|
||||||
|
//
|
||||||
|
// Due to this, at least one user in the Discord has witnessed behaviour where, when
|
||||||
|
// a MonitorPoll event is triggered by OLEChannelWnd, the focused monitor index gets
|
||||||
|
// set repeatedly to 0, regardless of where the current foreground window is actually
|
||||||
|
// located.
|
||||||
|
//
|
||||||
|
// This check ensures that we only update the focused monitor when the window
|
||||||
|
// triggering monitor reconciliation is known to not be tied to a specific monitor.
|
||||||
|
if window.class()? != "OleMainThreadWndClass" {
|
||||||
|
self.focus_monitor(monitor_idx)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let invisible_borders = self.invisible_borders;
|
||||||
|
let offset = self.work_area_offset;
|
||||||
|
|
||||||
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
||||||
let work_area = *monitor.work_area_size();
|
let work_area = *monitor.work_area_size();
|
||||||
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
|
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||||
let reaped_orphans = workspace.reap_orphans()?;
|
let reaped_orphans = workspace.reap_orphans()?;
|
||||||
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
|
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
|
||||||
workspace.update(&work_area)?;
|
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||||
reaped_orphans.0,
|
reaped_orphans.0,
|
||||||
@@ -91,13 +131,29 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
WindowManagerEvent::Minimize(_, window)
|
WindowManagerEvent::Raise(window) => {
|
||||||
| WindowManagerEvent::Destroy(_, window)
|
window.raise()?;
|
||||||
| WindowManagerEvent::Unmanage(window) => {
|
self.has_pending_raise_op = false;
|
||||||
|
}
|
||||||
|
WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => {
|
||||||
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||||
self.update_focused_workspace(false)?;
|
self.update_focused_workspace(false)?;
|
||||||
}
|
}
|
||||||
|
WindowManagerEvent::Minimize(_, window) => {
|
||||||
|
let mut hide = false;
|
||||||
|
|
||||||
|
{
|
||||||
|
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||||
|
if !programmatically_hidden_hwnds.contains(&window.hwnd) {
|
||||||
|
hide = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hide {
|
||||||
|
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||||
|
self.update_focused_workspace(false)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
WindowManagerEvent::Hide(_, window) => {
|
WindowManagerEvent::Hide(_, window) => {
|
||||||
let mut hide = false;
|
let mut hide = false;
|
||||||
// Some major applications unfortunately send the HIDE signal when they are being
|
// Some major applications unfortunately send the HIDE signal when they are being
|
||||||
@@ -105,16 +161,17 @@ impl WindowManager {
|
|||||||
// and will have is_window() return true, as the process is still running even if
|
// and will have is_window() return true, as the process is still running even if
|
||||||
// the window is not visible.
|
// the window is not visible.
|
||||||
{
|
{
|
||||||
let tray_and_multi_window_exes = TRAY_AND_MULTI_WINDOW_EXES.lock();
|
let tray_and_multi_window_identifiers =
|
||||||
let tray_and_multi_window_classes = TRAY_AND_MULTI_WINDOW_CLASSES.lock();
|
TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||||
|
|
||||||
// We don't want to purge windows that have been deliberately hidden by us, eg. when
|
// We don't want to purge windows that have been deliberately hidden by us, eg. when
|
||||||
// they are not on the top of a container stack.
|
// they are not on the top of a container stack.
|
||||||
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||||
|
|
||||||
if (!window.is_window() || tray_and_multi_window_exes.contains(&window.exe()?))
|
if ((!window.is_window()
|
||||||
|| tray_and_multi_window_classes.contains(&window.class()?)
|
|| tray_and_multi_window_identifiers.contains(&window.exe()?))
|
||||||
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
|
|| tray_and_multi_window_identifiers.contains(&window.class()?))
|
||||||
|
&& !programmatically_hidden_hwnds.contains(&window.hwnd)
|
||||||
{
|
{
|
||||||
hide = true;
|
hide = true;
|
||||||
}
|
}
|
||||||
@@ -154,16 +211,16 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(indices) = switch_to {
|
if let Some((known_monitor_idx, known_workspace_idx)) = switch_to {
|
||||||
if self.focused_monitor_idx() != indices.0
|
if self.focused_monitor_idx() != known_monitor_idx
|
||||||
&& self
|
|| self
|
||||||
.focused_monitor()
|
.focused_monitor()
|
||||||
.context("there is no monitor")?
|
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||||
.focused_workspace_idx()
|
.focused_workspace_idx()
|
||||||
!= indices.1
|
!= known_workspace_idx
|
||||||
{
|
{
|
||||||
self.focus_monitor(indices.0)?;
|
self.focus_monitor(known_monitor_idx)?;
|
||||||
self.focus_workspace(indices.1)?;
|
self.focus_workspace(known_workspace_idx)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,14 +246,54 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let behaviour = self.window_container_behaviour;
|
||||||
let workspace = self.focused_workspace_mut()?;
|
let workspace = self.focused_workspace_mut()?;
|
||||||
|
|
||||||
if !workspace.contains_window(window.hwnd) {
|
if !workspace.contains_window(window.hwnd) {
|
||||||
workspace.new_container_for_window(*window);
|
match behaviour {
|
||||||
self.update_focused_workspace(false)?;
|
WindowContainerBehaviour::Create => {
|
||||||
|
workspace.new_container_for_window(*window);
|
||||||
|
self.update_focused_workspace(false)?;
|
||||||
|
}
|
||||||
|
WindowContainerBehaviour::Append => {
|
||||||
|
workspace
|
||||||
|
.focused_container_mut()
|
||||||
|
.ok_or_else(|| anyhow!("there is no focused container"))?
|
||||||
|
.add_window(*window);
|
||||||
|
self.update_focused_workspace(true)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WindowManagerEvent::MoveResizeStart(_, _) => {
|
||||||
|
let monitor_idx = self.focused_monitor_idx();
|
||||||
|
let workspace_idx = self
|
||||||
|
.focused_monitor()
|
||||||
|
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||||
|
.focused_workspace_idx();
|
||||||
|
let container_idx = self
|
||||||
|
.focused_monitor()
|
||||||
|
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||||
|
.focused_workspace()
|
||||||
|
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
|
||||||
|
.focused_container_idx();
|
||||||
|
|
||||||
|
self.pending_move_op = Option::from((monitor_idx, workspace_idx, container_idx));
|
||||||
|
}
|
||||||
WindowManagerEvent::MoveResizeEnd(_, window) => {
|
WindowManagerEvent::MoveResizeEnd(_, window) => {
|
||||||
|
// We need this because if the event ends on a different monitor,
|
||||||
|
// that monitor will already have been focused and updated in the state
|
||||||
|
let pending = self.pending_move_op;
|
||||||
|
// Always consume the pending move op whenever this event is handled
|
||||||
|
self.pending_move_op = None;
|
||||||
|
|
||||||
|
let target_monitor_idx = self
|
||||||
|
.monitor_idx_from_current_pos()
|
||||||
|
.ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?;
|
||||||
|
|
||||||
|
let new_window_behaviour = self.window_container_behaviour;
|
||||||
|
let invisible_borders = self.invisible_borders;
|
||||||
|
|
||||||
let workspace = self.focused_workspace_mut()?;
|
let workspace = self.focused_workspace_mut()?;
|
||||||
if workspace
|
if workspace
|
||||||
.floating_windows()
|
.floating_windows()
|
||||||
@@ -206,26 +303,38 @@ impl WindowManager {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let focused_idx = workspace.focused_container_idx();
|
let focused_container_idx = workspace.focused_container_idx();
|
||||||
let old_position = *workspace
|
|
||||||
.latest_layout()
|
|
||||||
.get(focused_idx)
|
|
||||||
.context("there is no latest layout")?;
|
|
||||||
let mut new_position = WindowsApi::window_rect(window.hwnd())?;
|
let mut new_position = WindowsApi::window_rect(window.hwnd())?;
|
||||||
|
|
||||||
// See Window.set_position() in window.rs for comments
|
let old_position = *workspace
|
||||||
let border = Rect {
|
.latest_layout()
|
||||||
left: 12,
|
.get(focused_container_idx)
|
||||||
top: 0,
|
// If the move was to another monitor with an empty workspace, the
|
||||||
right: 24,
|
// workspace here will refer to that empty workspace, which won't
|
||||||
bottom: 12,
|
// have any latest layout set. We fall back to a Default for Rect
|
||||||
};
|
// which allows us to make a reasonable guess that the drag has taken
|
||||||
|
// place across a monitor boundary to an empty workspace
|
||||||
|
.unwrap_or(&Rect::default());
|
||||||
|
|
||||||
// Adjust for the invisible border
|
// This will be true if we have moved to an empty workspace on another monitor
|
||||||
new_position.left += border.left;
|
let mut moved_across_monitors = old_position == Rect::default();
|
||||||
new_position.top += border.top;
|
|
||||||
new_position.right -= border.right;
|
if let Some((origin_monitor_idx, _, _)) = pending {
|
||||||
new_position.bottom -= border.bottom;
|
// If we didn't move to another monitor with an empty workspace, it is
|
||||||
|
// still possible that we moved to another monitor with a populated workspace
|
||||||
|
if !moved_across_monitors {
|
||||||
|
// So we'll check if the origin monitor index and the target monitor index
|
||||||
|
// are different, if they are, we can set the override
|
||||||
|
moved_across_monitors = origin_monitor_idx != target_monitor_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust for the invisible borders
|
||||||
|
new_position.left += invisible_borders.left;
|
||||||
|
new_position.top += invisible_borders.top;
|
||||||
|
new_position.right -= invisible_borders.right;
|
||||||
|
new_position.bottom -= invisible_borders.bottom;
|
||||||
|
|
||||||
let resize = Rect {
|
let resize = Rect {
|
||||||
left: new_position.left - old_position.left,
|
left: new_position.left - old_position.left,
|
||||||
@@ -234,16 +343,93 @@ impl WindowManager {
|
|||||||
bottom: new_position.bottom - old_position.bottom,
|
bottom: new_position.bottom - old_position.bottom,
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_move = resize.right == 0 && resize.bottom == 0;
|
// If we have moved across the monitors, use that override, otherwise determine
|
||||||
|
// if a move has taken place by ruling out a resize
|
||||||
|
let is_move = moved_across_monitors
|
||||||
|
|| resize.right == 0 && resize.bottom == 0
|
||||||
|
|| resize.right.abs() == invisible_borders.right
|
||||||
|
&& resize.bottom.abs() == invisible_borders.bottom;
|
||||||
|
|
||||||
if is_move {
|
if is_move {
|
||||||
tracing::info!("moving with mouse");
|
tracing::info!("moving with mouse");
|
||||||
match workspace.container_idx_from_current_point() {
|
|
||||||
Some(target_idx) => {
|
if moved_across_monitors {
|
||||||
workspace.swap_containers(focused_idx, target_idx);
|
if let Some((
|
||||||
|
origin_monitor_idx,
|
||||||
|
origin_workspace_idx,
|
||||||
|
origin_container_idx,
|
||||||
|
)) = pending
|
||||||
|
{
|
||||||
|
let target_workspace_idx = self
|
||||||
|
.monitors()
|
||||||
|
.get(target_monitor_idx)
|
||||||
|
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
|
||||||
|
.focused_workspace_idx();
|
||||||
|
|
||||||
|
let target_container_idx = self
|
||||||
|
.monitors()
|
||||||
|
.get(target_monitor_idx)
|
||||||
|
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
|
||||||
|
.focused_workspace()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow!("there is no focused workspace for this monitor")
|
||||||
|
})?
|
||||||
|
.container_idx_from_current_point()
|
||||||
|
// Default to 0 in the case of an empty workspace
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
self.transfer_container(
|
||||||
|
(
|
||||||
|
origin_monitor_idx,
|
||||||
|
origin_workspace_idx,
|
||||||
|
origin_container_idx,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
target_monitor_idx,
|
||||||
|
target_workspace_idx,
|
||||||
|
target_container_idx,
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// We want to make sure both the origin and target monitors are updated,
|
||||||
|
// so that we don't have ghost tiles until we force an interaction on
|
||||||
|
// the origin monitor's focused workspace
|
||||||
|
self.focus_monitor(origin_monitor_idx)?;
|
||||||
|
self.focus_workspace(origin_workspace_idx)?;
|
||||||
|
self.update_focused_workspace(false)?;
|
||||||
|
|
||||||
|
self.focus_monitor(target_monitor_idx)?;
|
||||||
|
self.focus_workspace(target_workspace_idx)?;
|
||||||
self.update_focused_workspace(false)?;
|
self.update_focused_workspace(false)?;
|
||||||
}
|
}
|
||||||
None => self.update_focused_workspace(true)?,
|
// Here we handle a simple move on the same monitor which is treated as
|
||||||
|
// a container swap
|
||||||
|
} else {
|
||||||
|
match new_window_behaviour {
|
||||||
|
WindowContainerBehaviour::Create => {
|
||||||
|
match workspace.container_idx_from_current_point() {
|
||||||
|
Some(target_idx) => {
|
||||||
|
workspace
|
||||||
|
.swap_containers(focused_container_idx, target_idx);
|
||||||
|
self.update_focused_workspace(false)?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.update_focused_workspace(self.mouse_follows_focus)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowContainerBehaviour::Append => {
|
||||||
|
match workspace.container_idx_from_current_point() {
|
||||||
|
Some(target_idx) => {
|
||||||
|
workspace.move_window_to_container(target_idx)?;
|
||||||
|
self.update_focused_workspace(false)?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.update_focused_workspace(self.mouse_follows_focus)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("resizing with mouse");
|
tracing::info!("resizing with mouse");
|
||||||
@@ -278,19 +464,54 @@ impl WindowManager {
|
|||||||
ops.push(resize_op!(resize.bottom, <, OperationDirection::Down));
|
ops.push(resize_op!(resize.bottom, <, OperationDirection::Down));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (edge, sizing, step) in ops {
|
for (edge, sizing, delta) in ops {
|
||||||
self.resize_window(edge, sizing, Option::from(step))?;
|
self.resize_window(edge, sizing, delta, true)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_focused_workspace(false)?;
|
self.update_focused_workspace(false)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowManagerEvent::MouseCapture(..) => {}
|
WindowManagerEvent::MonitorPoll(..) | WindowManagerEvent::MouseCapture(..) => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if *self.focused_workspace()?.tile() && BORDER_ENABLED.load(Ordering::SeqCst) {
|
||||||
|
match event {
|
||||||
|
WindowManagerEvent::MoveResizeStart(_, _) => {
|
||||||
|
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||||
|
border.hide()?;
|
||||||
|
BORDER_HIDDEN.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
WindowManagerEvent::MoveResizeEnd(_, _)
|
||||||
|
| WindowManagerEvent::Show(_, _)
|
||||||
|
| WindowManagerEvent::FocusChange(_, _) => {
|
||||||
|
let window = self.focused_window()?;
|
||||||
|
let mut rect = WindowsApi::window_rect(window.hwnd())?;
|
||||||
|
rect.top -= self.invisible_borders.bottom;
|
||||||
|
rect.bottom += self.invisible_borders.bottom;
|
||||||
|
|
||||||
|
let activate = BORDER_HIDDEN.load(Ordering::SeqCst);
|
||||||
|
|
||||||
|
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||||
|
border.set_position(*window, &self.invisible_borders, activate)?;
|
||||||
|
|
||||||
|
if activate {
|
||||||
|
BORDER_HIDDEN.store(false, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
|
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
|
||||||
if let WindowManagerEvent::Unmanage(window) = event {
|
if let WindowManagerEvent::Unmanage(window) = event {
|
||||||
window.center(&self.focused_monitor_work_area()?)?;
|
window.center(&self.focused_monitor_work_area()?, &invisible_borders)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no more windows on the workspace, we shouldn't show the border window
|
||||||
|
if self.focused_workspace()?.containers().is_empty() {
|
||||||
|
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||||
|
border.hide()?;
|
||||||
|
BORDER_HIDDEN.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::trace!("updating list of known hwnds");
|
tracing::trace!("updating list of known hwnds");
|
||||||
@@ -305,8 +526,7 @@ impl WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut hwnd_json = dirs::home_dir().context("there is no home directory")?;
|
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
|
||||||
hwnd_json.push("komorebi.hwnd.json");
|
|
||||||
let file = OpenOptions::new()
|
let file = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
@@ -314,6 +534,11 @@ impl WindowManager {
|
|||||||
.open(hwnd_json)?;
|
.open(hwnd_json)?;
|
||||||
|
|
||||||
serde_json::to_writer_pretty(&file, &known_hwnds)?;
|
serde_json::to_writer_pretty(&file, &known_hwnds)?;
|
||||||
|
notify_subscribers(&serde_json::to_string(&Notification {
|
||||||
|
event: NotificationEvent::WindowManager(*event),
|
||||||
|
state: self.as_ref().into(),
|
||||||
|
})?)?;
|
||||||
|
|
||||||
tracing::info!("processed: {}", event.window().to_string());
|
tracing::info!("processed: {}", event.window().to_string());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
41
komorebi/src/process_movement.rs
Normal file
41
komorebi/src/process_movement.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use winput::message_loop;
|
||||||
|
use winput::message_loop::Event;
|
||||||
|
use winput::Action;
|
||||||
|
|
||||||
|
use komorebi_core::FocusFollowsMouseImplementation;
|
||||||
|
|
||||||
|
use crate::window_manager::WindowManager;
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub fn listen_for_movements(wm: Arc<Mutex<WindowManager>>) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut ignore_movement = false;
|
||||||
|
|
||||||
|
let receiver = message_loop::start().expect("could not start winput message loop");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let focus_follows_mouse = wm.lock().focus_follows_mouse;
|
||||||
|
if let Some(FocusFollowsMouseImplementation::Komorebi) = focus_follows_mouse {
|
||||||
|
match receiver.next_event() {
|
||||||
|
// Don't want to send any raise events while we are dragging or resizing
|
||||||
|
Event::MouseButton { action, .. } => match action {
|
||||||
|
Action::Press => ignore_movement = true,
|
||||||
|
Action::Release => ignore_movement = false,
|
||||||
|
},
|
||||||
|
Event::MouseMoveRelative { .. } => {
|
||||||
|
if !ignore_movement {
|
||||||
|
match wm.lock().raise_window_at_cursor_pos() {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(error) => tracing::error!("{}", error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||||
pub struct Ring<T> {
|
pub struct Ring<T> {
|
||||||
elements: VecDeque<T>,
|
elements: VecDeque<T>,
|
||||||
focused: usize,
|
focused: usize,
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SWP_ASYNCWINDOWPOS;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_ASYNCWINDOWPOS;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_DEFERERASE;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_DEFERERASE;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_DRAWFRAME;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_DRAWFRAME;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_FRAMECHANGED;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_FRAMECHANGED;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_HIDEWINDOW;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_HIDEWINDOW;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_NOACTIVATE;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOACTIVATE;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_NOCOPYBITS;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOCOPYBITS;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_NOMOVE;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOMOVE;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_NOOWNERZORDER;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOOWNERZORDER;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_NOREDRAW;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOREDRAW;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_NOREPOSITION;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOREPOSITION;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_NOSENDCHANGING;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOSENDCHANGING;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_NOSIZE;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOSIZE;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_NOZORDER;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_NOZORDER;
|
use windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
|
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_BORDER;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_CAPTION;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_CHILDWINDOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_CLIPCHILDREN;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_CLIPSIBLINGS;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_DLGFRAME;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_ACCEPTFILES;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_APPWINDOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_CLIENTEDGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_COMPOSITED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTEXTHELP;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTROLPARENT;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_DLGMODALFRAME;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYOUTRTL;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFT;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFTSCROLLBAR;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LTRREADING;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_MDICHILD;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOINHERITLAYOUT;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOPARENTNOTIFY;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOREDIRECTIONBITMAP;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_OVERLAPPEDWINDOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_PALETTEWINDOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHT;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHTSCROLLBAR;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_RTLREADING;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_STATICEDGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TRANSPARENT;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_WINDOWEDGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_GROUP;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_HSCROLL;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_ICONIC;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPEDWINDOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_POPUPWINDOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_SIZEBOX;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_TABSTOP;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_THICKFRAME;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_TILED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_TILEDWINDOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL;
|
||||||
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_BORDER;
|
// https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CAPTION;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CHILDWINDOW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CLIPCHILDREN;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_CLIPSIBLINGS;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_DLGFRAME;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_ACCEPTFILES;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_APPWINDOW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CLIENTEDGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_COMPOSITED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTEXTHELP;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_CONTROLPARENT;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_DLGMODALFRAME;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYOUTRTL;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFT;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LEFTSCROLLBAR;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_LTRREADING;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_MDICHILD;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOINHERITLAYOUT;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOPARENTNOTIFY;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_NOREDIRECTIONBITMAP;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_OVERLAPPEDWINDOW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_PALETTEWINDOW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHT;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RIGHTSCROLLBAR;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_RTLREADING;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_STATICEDGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_TRANSPARENT;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_EX_WINDOWEDGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_GROUP;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_HSCROLL;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_ICONIC;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_OVERLAPPEDWINDOW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_POPUPWINDOW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_SIZEBOX;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TABSTOP;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_THICKFRAME;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TILED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_TILEDWINDOW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL;
|
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GwlStyle: u32 {
|
pub struct WindowStyle: u32 {
|
||||||
const BORDER = WS_BORDER.0;
|
const BORDER = WS_BORDER.0;
|
||||||
const CAPTION = WS_CAPTION.0;
|
const CAPTION = WS_CAPTION.0;
|
||||||
const CHILD = WS_CHILD.0;
|
const CHILD = WS_CHILD.0;
|
||||||
@@ -88,9 +88,10 @@ bitflags! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GwlExStyle: u32 {
|
pub struct ExtendedWindowStyle: u32 {
|
||||||
const ACCEPTFILES = WS_EX_ACCEPTFILES.0;
|
const ACCEPTFILES = WS_EX_ACCEPTFILES.0;
|
||||||
const APPWINDOW = WS_EX_APPWINDOW.0;
|
const APPWINDOW = WS_EX_APPWINDOW.0;
|
||||||
const CLIENTEDGE = WS_EX_CLIENTEDGE.0;
|
const CLIENTEDGE = WS_EX_CLIENTEDGE.0;
|
||||||
|
|||||||
@@ -1,26 +1,34 @@
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::fmt::Formatter;
|
use std::fmt::Formatter;
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
use color_eyre::eyre::ContextCompat;
|
use color_eyre::eyre::anyhow;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::ser::Error;
|
||||||
use serde::ser::SerializeStruct;
|
use serde::ser::SerializeStruct;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::Serializer;
|
use serde::Serializer;
|
||||||
|
use windows::Win32::Foundation::HWND;
|
||||||
|
|
||||||
use bindings::Windows::Win32::Foundation::HWND;
|
use komorebi_core::ApplicationIdentifier;
|
||||||
|
use komorebi_core::HidingBehaviour;
|
||||||
use komorebi_core::Rect;
|
use komorebi_core::Rect;
|
||||||
|
|
||||||
use crate::styles::GwlExStyle;
|
use crate::styles::ExtendedWindowStyle;
|
||||||
use crate::styles::GwlStyle;
|
use crate::styles::WindowStyle;
|
||||||
use crate::window_manager_event::WindowManagerEvent;
|
use crate::window_manager_event::WindowManagerEvent;
|
||||||
use crate::windows_api::WindowsApi;
|
use crate::windows_api::WindowsApi;
|
||||||
|
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||||
use crate::FLOAT_IDENTIFIERS;
|
use crate::FLOAT_IDENTIFIERS;
|
||||||
use crate::HIDDEN_HWNDS;
|
use crate::HIDDEN_HWNDS;
|
||||||
use crate::LAYERED_EXE_WHITELIST;
|
use crate::HIDING_BEHAVIOUR;
|
||||||
|
use crate::LAYERED_WHITELIST;
|
||||||
use crate::MANAGE_IDENTIFIERS;
|
use crate::MANAGE_IDENTIFIERS;
|
||||||
|
use crate::WSL2_UI_PROCESSES;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, JsonSchema)]
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
pub(crate) hwnd: isize,
|
pub(crate) hwnd: isize,
|
||||||
}
|
}
|
||||||
@@ -30,18 +38,18 @@ impl Display for Window {
|
|||||||
let mut display = format!("(hwnd: {}", self.hwnd);
|
let mut display = format!("(hwnd: {}", self.hwnd);
|
||||||
|
|
||||||
if let Ok(title) = self.title() {
|
if let Ok(title) = self.title() {
|
||||||
display.push_str(&format!(", title: {}", title));
|
write!(display, ", title: {}", title)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(exe) = self.exe() {
|
if let Ok(exe) = self.exe() {
|
||||||
display.push_str(&format!(", exe: {}", exe));
|
write!(display, ", exe: {}", exe)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(class) = self.class() {
|
if let Ok(class) = self.class() {
|
||||||
display.push_str(&format!(", class: {}", class));
|
write!(display, ", class: {}", class)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
display.push(')');
|
write!(display, ")")?;
|
||||||
|
|
||||||
write!(f, "{}", display)
|
write!(f, "{}", display)
|
||||||
}
|
}
|
||||||
@@ -54,12 +62,28 @@ impl Serialize for Window {
|
|||||||
{
|
{
|
||||||
let mut state = serializer.serialize_struct("Window", 5)?;
|
let mut state = serializer.serialize_struct("Window", 5)?;
|
||||||
state.serialize_field("hwnd", &self.hwnd)?;
|
state.serialize_field("hwnd", &self.hwnd)?;
|
||||||
state.serialize_field("title", &self.title().expect("could not get window title"))?;
|
state.serialize_field(
|
||||||
state.serialize_field("exe", &self.exe().expect("could not get window exe"))?;
|
"title",
|
||||||
state.serialize_field("class", &self.class().expect("could not get window class"))?;
|
&self
|
||||||
|
.title()
|
||||||
|
.map_err(|_| S::Error::custom("could not get window title"))?,
|
||||||
|
)?;
|
||||||
|
state.serialize_field(
|
||||||
|
"exe",
|
||||||
|
&self
|
||||||
|
.exe()
|
||||||
|
.map_err(|_| S::Error::custom("could not get window exe"))?,
|
||||||
|
)?;
|
||||||
|
state.serialize_field(
|
||||||
|
"class",
|
||||||
|
&self
|
||||||
|
.class()
|
||||||
|
.map_err(|_| S::Error::custom("could not get window class"))?,
|
||||||
|
)?;
|
||||||
state.serialize_field(
|
state.serialize_field(
|
||||||
"rect",
|
"rect",
|
||||||
&WindowsApi::window_rect(self.hwnd()).expect("could not get window rect"),
|
&WindowsApi::window_rect(self.hwnd())
|
||||||
|
.map_err(|_| S::Error::custom("could not get window rect"))?,
|
||||||
)?;
|
)?;
|
||||||
state.end()
|
state.end()
|
||||||
}
|
}
|
||||||
@@ -70,7 +94,7 @@ impl Window {
|
|||||||
HWND(self.hwnd)
|
HWND(self.hwnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
|
pub fn center(&mut self, work_area: &Rect, invisible_borders: &Rect) -> Result<()> {
|
||||||
let half_width = work_area.right / 2;
|
let half_width = work_area.right / 2;
|
||||||
let half_weight = work_area.bottom / 2;
|
let half_weight = work_area.bottom / 2;
|
||||||
|
|
||||||
@@ -81,44 +105,35 @@ impl Window {
|
|||||||
right: half_width,
|
right: half_width,
|
||||||
bottom: half_weight,
|
bottom: half_weight,
|
||||||
},
|
},
|
||||||
|
invisible_borders,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
|
pub fn set_position(
|
||||||
// NOTE: This is how the border variable below was calculated; every time this code was
|
&mut self,
|
||||||
// run on any window in any position, the generated border was always the same, so I am
|
layout: &Rect,
|
||||||
// hard coding the border Rect to avoid two calls to set_window_pos and making the screen
|
invisible_borders: &Rect,
|
||||||
// flicker on container/window movement. Still not 100% sure if this is DPI-aware.
|
top: bool,
|
||||||
|
) -> Result<()> {
|
||||||
// Set the new position first to be able to get the extended frame bounds
|
|
||||||
// WindowsApi::set_window_pos(self.hwnd(), layout, false, false)?;
|
|
||||||
// let mut rect = WindowsApi::window_rect(self.hwnd())?;
|
|
||||||
|
|
||||||
// Get the extended frame bounds of the new position
|
|
||||||
// let frame = WindowsApi::window_rect_with_extended_frame_bounds(self.hwnd())?;
|
|
||||||
|
|
||||||
// Calculate the invisible border diff
|
|
||||||
// let border = Rect {
|
|
||||||
// left: frame.left - rect.left,
|
|
||||||
// top: frame.top - rect.top,
|
|
||||||
// right: rect.right - frame.right,
|
|
||||||
// bottom: rect.bottom - frame.bottom,
|
|
||||||
// };
|
|
||||||
|
|
||||||
let mut rect = *layout;
|
let mut rect = *layout;
|
||||||
let border = Rect {
|
let mut should_remove_border = true;
|
||||||
left: 12,
|
|
||||||
top: 0,
|
|
||||||
right: 24,
|
|
||||||
bottom: 12,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove the invisible border
|
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||||
rect.left -= border.left;
|
if border_overflows.contains(&self.title()?)
|
||||||
rect.top -= border.top;
|
|| border_overflows.contains(&self.exe()?)
|
||||||
rect.right += border.right;
|
|| border_overflows.contains(&self.class()?)
|
||||||
rect.bottom += border.bottom;
|
{
|
||||||
|
should_remove_border = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if should_remove_border {
|
||||||
|
// Remove the invisible borders
|
||||||
|
rect.left -= invisible_borders.left;
|
||||||
|
rect.top -= invisible_borders.top;
|
||||||
|
rect.right += invisible_borders.right;
|
||||||
|
rect.bottom += invisible_borders.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
WindowsApi::position_window(self.hwnd(), &rect, top)
|
WindowsApi::position_window(self.hwnd(), &rect, top)
|
||||||
}
|
}
|
||||||
@@ -129,7 +144,11 @@ impl Window {
|
|||||||
programmatically_hidden_hwnds.push(self.hwnd);
|
programmatically_hidden_hwnds.push(self.hwnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowsApi::hide_window(self.hwnd());
|
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
|
||||||
|
match *hiding_behaviour {
|
||||||
|
HidingBehaviour::Hide => WindowsApi::hide_window(self.hwnd()),
|
||||||
|
HidingBehaviour::Minimize => WindowsApi::minimize_window(self.hwnd()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore(self) {
|
pub fn restore(self) {
|
||||||
@@ -156,7 +175,19 @@ impl Window {
|
|||||||
WindowsApi::maximize_window(self.hwnd());
|
WindowsApi::maximize_window(self.hwnd());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focus(self) -> Result<()> {
|
pub fn unmaximize(self) {
|
||||||
|
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||||
|
if let Some(idx) = programmatically_hidden_hwnds
|
||||||
|
.iter()
|
||||||
|
.position(|&hwnd| hwnd == self.hwnd)
|
||||||
|
{
|
||||||
|
programmatically_hidden_hwnds.remove(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowsApi::unmaximize_window(self.hwnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn raise(self) -> Result<()> {
|
||||||
// Attach komorebi thread to Window thread
|
// Attach komorebi thread to Window thread
|
||||||
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
|
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||||
let current_thread_id = WindowsApi::current_thread_id();
|
let current_thread_id = WindowsApi::current_thread_id();
|
||||||
@@ -173,26 +204,71 @@ impl Window {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Center cursor in Window
|
|
||||||
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
|
|
||||||
|
|
||||||
// This isn't really needed when the above command works as expected via AHK
|
// This isn't really needed when the above command works as expected via AHK
|
||||||
WindowsApi::set_focus(self.hwnd())
|
WindowsApi::set_focus(self.hwnd())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn focus(self, mouse_follows_focus: bool) -> Result<()> {
|
||||||
|
// Attach komorebi thread to Window thread
|
||||||
|
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||||
|
let current_thread_id = WindowsApi::current_thread_id();
|
||||||
|
|
||||||
|
// This can be allowed to fail if a window doesn't have a message queue or if a journal record
|
||||||
|
// hook has been installed
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput#remarks
|
||||||
|
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
"could not attach to window thread input processing mechanism, but continuing execution of focus(): {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Raise Window to foreground
|
||||||
|
match WindowsApi::set_foreground_window(self.hwnd()) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
"could not set as foreground window, but continuing execution of focus(): {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Center cursor in Window
|
||||||
|
if mouse_follows_focus {
|
||||||
|
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This isn't really needed when the above command works as expected via AHK
|
||||||
|
match WindowsApi::set_focus(self.hwnd()) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
"could not set focus, but continuing execution of focus(): {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn update_style(self, style: GwlStyle) -> Result<()> {
|
pub fn update_style(self, style: WindowStyle) -> Result<()> {
|
||||||
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
|
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn style(self) -> Result<GwlStyle> {
|
pub fn style(self) -> Result<WindowStyle> {
|
||||||
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?;
|
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?;
|
||||||
GwlStyle::from_bits(bits).context("there is no gwl style")
|
WindowStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ex_style(self) -> Result<GwlExStyle> {
|
pub fn ex_style(self) -> Result<ExtendedWindowStyle> {
|
||||||
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?;
|
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?;
|
||||||
GwlExStyle::from_bits(bits).context("there is no gwl style")
|
ExtendedWindowStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn title(self) -> Result<String> {
|
pub fn title(self) -> Result<String> {
|
||||||
@@ -218,6 +294,11 @@ impl Window {
|
|||||||
|
|
||||||
#[tracing::instrument(fields(exe, title))]
|
#[tracing::instrument(fields(exe, title))]
|
||||||
pub fn should_manage(self, event: Option<WindowManagerEvent>) -> Result<bool> {
|
pub fn should_manage(self, event: Option<WindowManagerEvent>) -> Result<bool> {
|
||||||
|
if let Some(WindowManagerEvent::MonitorPoll(_, _)) = event {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::question_mark)]
|
||||||
if self.title().is_err() {
|
if self.title().is_err() {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
@@ -235,42 +316,7 @@ impl Window {
|
|||||||
// If not allowing cloaked windows, we need to ensure the window is not cloaked
|
// If not allowing cloaked windows, we need to ensure the window is not cloaked
|
||||||
(false, false) => {
|
(false, false) => {
|
||||||
if let (Ok(title), Ok(exe_name), Ok(class)) = (self.title(), self.exe(), self.class()) {
|
if let (Ok(title), Ok(exe_name), Ok(class)) = (self.title(), self.exe(), self.class()) {
|
||||||
{
|
return Ok(window_is_eligible(&title, &exe_name, &class, self.style()?, self.ex_style()?, event));
|
||||||
let float_identifiers = FLOAT_IDENTIFIERS.lock();
|
|
||||||
if float_identifiers.contains(&title)
|
|
||||||
|| float_identifiers.contains(&exe_name)
|
|
||||||
|| float_identifiers.contains(&class) {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let managed_override = {
|
|
||||||
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
|
||||||
manage_identifiers.contains(&exe_name) || manage_identifiers.contains(&class)
|
|
||||||
};
|
|
||||||
|
|
||||||
let allow_layered = {
|
|
||||||
let layered_exe_whitelist = LAYERED_EXE_WHITELIST.lock();
|
|
||||||
layered_exe_whitelist.contains(&exe_name)
|
|
||||||
};
|
|
||||||
|
|
||||||
let style = self.style()?;
|
|
||||||
let ex_style = self.ex_style()?;
|
|
||||||
|
|
||||||
if style.contains(GwlStyle::CAPTION)
|
|
||||||
&& ex_style.contains(GwlExStyle::WINDOWEDGE)
|
|
||||||
&& !ex_style.contains(GwlExStyle::DLGMODALFRAME)
|
|
||||||
// Get a lot of dupe events coming through that make the redrawing go crazy
|
|
||||||
// on FocusChange events if I don't filter out this one. But, if we are
|
|
||||||
// allowing a specific layered window on the whitelist (like Steam), it should
|
|
||||||
// pass this check
|
|
||||||
&& (allow_layered || !ex_style.contains(GwlExStyle::LAYERED))
|
|
||||||
|| managed_override
|
|
||||||
{
|
|
||||||
return Ok(true);
|
|
||||||
} else if event.is_some() {
|
|
||||||
tracing::debug!("ignoring (exe: {}, title: {})", exe_name, title);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -279,3 +325,83 @@ impl Window {
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn window_is_eligible(
|
||||||
|
title: &String,
|
||||||
|
exe_name: &String,
|
||||||
|
class: &String,
|
||||||
|
style: WindowStyle,
|
||||||
|
ex_style: ExtendedWindowStyle,
|
||||||
|
event: Option<WindowManagerEvent>,
|
||||||
|
) -> bool {
|
||||||
|
let mut should_float = false;
|
||||||
|
let mut matched_identifier = None;
|
||||||
|
|
||||||
|
{
|
||||||
|
let float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||||
|
for identifier in float_identifiers.iter() {
|
||||||
|
if title.starts_with(identifier) || title.ends_with(identifier) {
|
||||||
|
should_float = true;
|
||||||
|
matched_identifier = Option::from(ApplicationIdentifier::Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
if class.starts_with(identifier) || class.ends_with(identifier) {
|
||||||
|
should_float = true;
|
||||||
|
matched_identifier = Option::from(ApplicationIdentifier::Class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if identifier == exe_name {
|
||||||
|
should_float = true;
|
||||||
|
matched_identifier = Option::from(ApplicationIdentifier::Exe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let managed_override = {
|
||||||
|
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||||
|
matched_identifier.map_or_else(
|
||||||
|
|| {
|
||||||
|
manage_identifiers.contains(exe_name)
|
||||||
|
|| manage_identifiers.contains(class)
|
||||||
|
|| manage_identifiers.contains(title)
|
||||||
|
},
|
||||||
|
|matched_identifier| match matched_identifier {
|
||||||
|
ApplicationIdentifier::Exe => manage_identifiers.contains(exe_name),
|
||||||
|
ApplicationIdentifier::Class => manage_identifiers.contains(class),
|
||||||
|
ApplicationIdentifier::Title => manage_identifiers.contains(title),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_float && !managed_override {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let allow_layered = {
|
||||||
|
let layered_whitelist = LAYERED_WHITELIST.lock();
|
||||||
|
layered_whitelist.contains(exe_name)
|
||||||
|
|| layered_whitelist.contains(class)
|
||||||
|
|| layered_whitelist.contains(title)
|
||||||
|
};
|
||||||
|
|
||||||
|
let allow_wsl2_gui = {
|
||||||
|
let wsl2_ui_processes = WSL2_UI_PROCESSES.lock();
|
||||||
|
wsl2_ui_processes.contains(exe_name)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (allow_wsl2_gui || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
|
||||||
|
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
|
||||||
|
// Get a lot of dupe events coming through that make the redrawing go crazy
|
||||||
|
// on FocusChange events if I don't filter out this one. But, if we are
|
||||||
|
// allowing a specific layered window on the whitelist (like Steam), it should
|
||||||
|
// pass this check
|
||||||
|
&& (allow_layered || !ex_style.contains(ExtendedWindowStyle::LAYERED))
|
||||||
|
|| managed_override
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
} else if event.is_some() {
|
||||||
|
tracing::debug!("ignoring (exe: {}, title: {})", exe_name, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +1,89 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::fmt::Formatter;
|
use std::fmt::Formatter;
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::window::Window;
|
use crate::window::Window;
|
||||||
use crate::winevent::WinEvent;
|
use crate::winevent::WinEvent;
|
||||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone, Serialize, JsonSchema)]
|
||||||
|
#[serde(tag = "type", content = "content")]
|
||||||
pub enum WindowManagerEvent {
|
pub enum WindowManagerEvent {
|
||||||
Destroy(WinEvent, Window),
|
Destroy(WinEvent, Window),
|
||||||
FocusChange(WinEvent, Window),
|
FocusChange(WinEvent, Window),
|
||||||
Hide(WinEvent, Window),
|
Hide(WinEvent, Window),
|
||||||
Minimize(WinEvent, Window),
|
Minimize(WinEvent, Window),
|
||||||
Show(WinEvent, Window),
|
Show(WinEvent, Window),
|
||||||
|
MoveResizeStart(WinEvent, Window),
|
||||||
MoveResizeEnd(WinEvent, Window),
|
MoveResizeEnd(WinEvent, Window),
|
||||||
MouseCapture(WinEvent, Window),
|
MouseCapture(WinEvent, Window),
|
||||||
Manage(Window),
|
Manage(Window),
|
||||||
Unmanage(Window),
|
Unmanage(Window),
|
||||||
|
Raise(Window),
|
||||||
|
MonitorPoll(WinEvent, Window),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for WindowManagerEvent {
|
impl Display for WindowManagerEvent {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
WindowManagerEvent::Manage(window) => {
|
Self::Manage(window) => {
|
||||||
write!(f, "Manage (Window: {})", window)
|
write!(f, "Manage (Window: {})", window)
|
||||||
}
|
}
|
||||||
WindowManagerEvent::Unmanage(window) => {
|
Self::Unmanage(window) => {
|
||||||
write!(f, "Unmanage (Window: {})", window)
|
write!(f, "Unmanage (Window: {})", window)
|
||||||
}
|
}
|
||||||
WindowManagerEvent::Destroy(winevent, window) => {
|
Self::Destroy(winevent, window) => {
|
||||||
write!(f, "Destroy (WinEvent: {}, Window: {})", winevent, window)
|
write!(f, "Destroy (WinEvent: {}, Window: {})", winevent, window)
|
||||||
}
|
}
|
||||||
WindowManagerEvent::FocusChange(winevent, window) => {
|
Self::FocusChange(winevent, window) => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"FocusChange (WinEvent: {}, Window: {})",
|
"FocusChange (WinEvent: {}, Window: {})",
|
||||||
winevent, window
|
winevent, window
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
WindowManagerEvent::Hide(winevent, window) => {
|
Self::Hide(winevent, window) => {
|
||||||
write!(f, "Hide (WinEvent: {}, Window: {})", winevent, window)
|
write!(f, "Hide (WinEvent: {}, Window: {})", winevent, window)
|
||||||
}
|
}
|
||||||
WindowManagerEvent::Minimize(winevent, window) => {
|
Self::Minimize(winevent, window) => {
|
||||||
write!(f, "Minimize (WinEvent: {}, Window: {})", winevent, window)
|
write!(f, "Minimize (WinEvent: {}, Window: {})", winevent, window)
|
||||||
}
|
}
|
||||||
WindowManagerEvent::Show(winevent, window) => {
|
Self::Show(winevent, window) => {
|
||||||
write!(f, "Show (WinEvent: {}, Window: {})", winevent, window)
|
write!(f, "Show (WinEvent: {}, Window: {})", winevent, window)
|
||||||
}
|
}
|
||||||
WindowManagerEvent::MoveResizeEnd(winevent, window) => {
|
Self::MoveResizeStart(winevent, window) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"MoveResizeStart (WinEvent: {}, Window: {})",
|
||||||
|
winevent, window
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Self::MoveResizeEnd(winevent, window) => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"MoveResizeEnd (WinEvent: {}, Window: {})",
|
"MoveResizeEnd (WinEvent: {}, Window: {})",
|
||||||
winevent, window
|
winevent, window
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
WindowManagerEvent::MouseCapture(winevent, window) => {
|
Self::MouseCapture(winevent, window) => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"MouseCapture (WinEvent: {}, Window: {})",
|
"MouseCapture (WinEvent: {}, Window: {})",
|
||||||
winevent, window
|
winevent, window
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Self::Raise(window) => {
|
||||||
|
write!(f, "Raise (Window: {})", window)
|
||||||
|
}
|
||||||
|
Self::MonitorPoll(winevent, window) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"MonitorPoll (WinEvent: {}, Window: {})",
|
||||||
|
winevent, window
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,15 +91,18 @@ impl Display for WindowManagerEvent {
|
|||||||
impl WindowManagerEvent {
|
impl WindowManagerEvent {
|
||||||
pub const fn window(self) -> Window {
|
pub const fn window(self) -> Window {
|
||||||
match self {
|
match self {
|
||||||
WindowManagerEvent::Destroy(_, window)
|
Self::Destroy(_, window)
|
||||||
| WindowManagerEvent::FocusChange(_, window)
|
| Self::FocusChange(_, window)
|
||||||
| WindowManagerEvent::Hide(_, window)
|
| Self::Hide(_, window)
|
||||||
| WindowManagerEvent::Minimize(_, window)
|
| Self::Minimize(_, window)
|
||||||
| WindowManagerEvent::Show(_, window)
|
| Self::Show(_, window)
|
||||||
| WindowManagerEvent::MoveResizeEnd(_, window)
|
| Self::MoveResizeStart(_, window)
|
||||||
| WindowManagerEvent::MouseCapture(_, window)
|
| Self::MoveResizeEnd(_, window)
|
||||||
| WindowManagerEvent::Manage(window)
|
| Self::MouseCapture(_, window)
|
||||||
| WindowManagerEvent::Unmanage(window) => window,
|
| Self::MonitorPoll(_, window)
|
||||||
|
| Self::Raise(window)
|
||||||
|
| Self::Manage(window)
|
||||||
|
| Self::Unmanage(window) => window,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +123,7 @@ impl WindowManagerEvent {
|
|||||||
WinEvent::ObjectFocus | WinEvent::SystemForeground => {
|
WinEvent::ObjectFocus | WinEvent::SystemForeground => {
|
||||||
Option::from(Self::FocusChange(winevent, window))
|
Option::from(Self::FocusChange(winevent, window))
|
||||||
}
|
}
|
||||||
|
WinEvent::SystemMoveSizeStart => Option::from(Self::MoveResizeStart(winevent, window)),
|
||||||
WinEvent::SystemMoveSizeEnd => Option::from(Self::MoveResizeEnd(winevent, window)),
|
WinEvent::SystemMoveSizeEnd => Option::from(Self::MoveResizeEnd(winevent, window)),
|
||||||
WinEvent::SystemCaptureStart | WinEvent::SystemCaptureEnd => {
|
WinEvent::SystemCaptureStart | WinEvent::SystemCaptureEnd => {
|
||||||
Option::from(Self::MouseCapture(winevent, window))
|
Option::from(Self::MouseCapture(winevent, window))
|
||||||
@@ -110,12 +138,26 @@ impl WindowManagerEvent {
|
|||||||
|
|
||||||
let object_name_change_on_launch = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
let object_name_change_on_launch = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||||
|
|
||||||
if object_name_change_on_launch.contains(&window.exe().ok()?) {
|
if object_name_change_on_launch.contains(&window.exe().ok()?)
|
||||||
|
|| object_name_change_on_launch.contains(&window.class().ok()?)
|
||||||
|
|| object_name_change_on_launch.contains(&window.title().ok()?)
|
||||||
|
{
|
||||||
Option::from(Self::Show(winevent, window))
|
Option::from(Self::Show(winevent, window))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WinEvent::ObjectCreate => {
|
||||||
|
if let Ok(title) = window.title() {
|
||||||
|
// Hidden COM support mechanism window that fires this event on both DPI/scaling
|
||||||
|
// changes and resolution changes, a good candidate for polling
|
||||||
|
if title == "OLEChannelWnd" {
|
||||||
|
return Option::from(Self::MonitorPoll(winevent, window));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +1,110 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
|
|
||||||
use color_eyre::eyre::ContextCompat;
|
use color_eyre::eyre::anyhow;
|
||||||
|
use color_eyre::eyre::Error;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use eyre::Error;
|
use windows::core::Result as WindowsCrateResult;
|
||||||
|
use windows::core::PCSTR;
|
||||||
|
use windows::core::PWSTR;
|
||||||
|
use windows::Win32::Foundation::BOOL;
|
||||||
|
use windows::Win32::Foundation::HANDLE;
|
||||||
|
use windows::Win32::Foundation::HINSTANCE;
|
||||||
|
use windows::Win32::Foundation::HWND;
|
||||||
|
use windows::Win32::Foundation::LPARAM;
|
||||||
|
use windows::Win32::Foundation::POINT;
|
||||||
|
use windows::Win32::Foundation::RECT;
|
||||||
|
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
|
||||||
|
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
|
||||||
|
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
|
||||||
|
use windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
|
||||||
|
use windows::Win32::Graphics::Dwm::DWMWA_WINDOW_CORNER_PREFERENCE;
|
||||||
|
use windows::Win32::Graphics::Dwm::DWMWCP_ROUND;
|
||||||
|
use windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE;
|
||||||
|
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
|
||||||
|
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
|
||||||
|
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
|
||||||
|
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
|
||||||
|
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
|
||||||
|
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
|
||||||
|
use windows::Win32::Graphics::Gdi::MonitorFromPoint;
|
||||||
|
use windows::Win32::Graphics::Gdi::MonitorFromWindow;
|
||||||
|
use windows::Win32::Graphics::Gdi::HBRUSH;
|
||||||
|
use windows::Win32::Graphics::Gdi::HDC;
|
||||||
|
use windows::Win32::Graphics::Gdi::HMONITOR;
|
||||||
|
use windows::Win32::Graphics::Gdi::MONITORENUMPROC;
|
||||||
|
use windows::Win32::Graphics::Gdi::MONITORINFO;
|
||||||
|
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
|
||||||
|
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||||
|
use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId;
|
||||||
|
use windows::Win32::System::Threading::AttachThreadInput;
|
||||||
|
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||||
|
use windows::Win32::System::Threading::GetCurrentThreadId;
|
||||||
|
use windows::Win32::System::Threading::OpenProcess;
|
||||||
|
use windows::Win32::System::Threading::QueryFullProcessImageNameW;
|
||||||
|
use windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
|
||||||
|
use windows::Win32::System::Threading::PROCESS_NAME_WIN32;
|
||||||
|
use windows::Win32::System::Threading::PROCESS_QUERY_INFORMATION;
|
||||||
|
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
|
||||||
|
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||||
|
use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus;
|
||||||
|
use windows::Win32::UI::Shell::Common::DEVICE_SCALE_FACTOR;
|
||||||
|
use windows::Win32::UI::Shell::GetScaleFactorForMonitor;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExA;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetTopWindow;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetWindow;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetWindowRect;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetWindowTextW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::IsIconic;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::IsWindow;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::RegisterClassA;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SetClassLongPtrW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SetWindowPos;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::CW_USEDEFAULT;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GCLP_HBRBACKGROUND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::HWND_BOTTOM;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::HWND_TOPMOST;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SPI_GETACTIVEWINDOWTRACKING;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SW_MINIMIZE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SW_NORMAL;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNOACTIVATE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_ACTION;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSA;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
|
||||||
|
|
||||||
use bindings::Windows::Win32::Foundation::BOOL;
|
|
||||||
use bindings::Windows::Win32::Foundation::HANDLE;
|
|
||||||
use bindings::Windows::Win32::Foundation::HWND;
|
|
||||||
use bindings::Windows::Win32::Foundation::LPARAM;
|
|
||||||
use bindings::Windows::Win32::Foundation::POINT;
|
|
||||||
use bindings::Windows::Win32::Foundation::PWSTR;
|
|
||||||
use bindings::Windows::Win32::Foundation::RECT;
|
|
||||||
use bindings::Windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
|
|
||||||
use bindings::Windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
|
|
||||||
use bindings::Windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
|
|
||||||
use bindings::Windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE;
|
|
||||||
use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
|
|
||||||
use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
|
|
||||||
use bindings::Windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
|
|
||||||
use bindings::Windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
|
|
||||||
use bindings::Windows::Win32::Graphics::Gdi::GetMonitorInfoW;
|
|
||||||
use bindings::Windows::Win32::Graphics::Gdi::MonitorFromWindow;
|
|
||||||
use bindings::Windows::Win32::Graphics::Gdi::HDC;
|
|
||||||
use bindings::Windows::Win32::Graphics::Gdi::HMONITOR;
|
|
||||||
use bindings::Windows::Win32::Graphics::Gdi::MONITORENUMPROC;
|
|
||||||
use bindings::Windows::Win32::Graphics::Gdi::MONITORINFO;
|
|
||||||
use bindings::Windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
|
|
||||||
use bindings::Windows::Win32::System::Threading::AttachThreadInput;
|
|
||||||
use bindings::Windows::Win32::System::Threading::GetCurrentProcessId;
|
|
||||||
use bindings::Windows::Win32::System::Threading::GetCurrentThreadId;
|
|
||||||
use bindings::Windows::Win32::System::Threading::OpenProcess;
|
|
||||||
use bindings::Windows::Win32::System::Threading::QueryFullProcessImageNameW;
|
|
||||||
use bindings::Windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
|
|
||||||
use bindings::Windows::Win32::System::Threading::PROCESS_NAME_FORMAT;
|
|
||||||
use bindings::Windows::Win32::System::Threading::PROCESS_QUERY_INFORMATION;
|
|
||||||
use bindings::Windows::Win32::UI::KeyboardAndMouseInput::SetFocus;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EnumWindows;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetTopWindow;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindow;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowRect;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowTextW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsIconic;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsWindow;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SetWindowPos;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::ShowWindow;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SystemParametersInfoW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::HWND_TOPMOST;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SW_RESTORE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_ACTION;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WINDOW_LONG_PTR_INDEX;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
|
|
||||||
use komorebi_core::Rect;
|
use komorebi_core::Rect;
|
||||||
|
|
||||||
use crate::container::Container;
|
use crate::container::Container;
|
||||||
@@ -83,43 +113,12 @@ use crate::monitor::Monitor;
|
|||||||
use crate::ring::Ring;
|
use crate::ring::Ring;
|
||||||
use crate::set_window_position::SetWindowPosition;
|
use crate::set_window_position::SetWindowPosition;
|
||||||
use crate::windows_callbacks;
|
use crate::windows_callbacks;
|
||||||
use crate::workspace::Workspace;
|
|
||||||
|
|
||||||
pub enum WindowsResult<T, E> {
|
pub enum WindowsResult<T, E> {
|
||||||
Err(E),
|
Err(E),
|
||||||
Ok(T),
|
Ok(T),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BOOL> for WindowsResult<(), Error> {
|
|
||||||
fn from(return_value: BOOL) -> Self {
|
|
||||||
if return_value.as_bool() {
|
|
||||||
Self::Ok(())
|
|
||||||
} else {
|
|
||||||
Self::Err(std::io::Error::last_os_error().into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HWND> for WindowsResult<isize, Error> {
|
|
||||||
fn from(return_value: HWND) -> Self {
|
|
||||||
if return_value.is_null() {
|
|
||||||
Self::Err(std::io::Error::last_os_error().into())
|
|
||||||
} else {
|
|
||||||
Self::Ok(return_value.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<HANDLE> for WindowsResult<HANDLE, Error> {
|
|
||||||
fn from(return_value: HANDLE) -> Self {
|
|
||||||
if return_value.is_null() {
|
|
||||||
Self::Err(std::io::Error::last_os_error().into())
|
|
||||||
} else {
|
|
||||||
Self::Ok(return_value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_from_integer_for_windows_result {
|
macro_rules! impl_from_integer_for_windows_result {
|
||||||
( $( $integer_type:ty ),+ ) => {
|
( $( $integer_type:ty ),+ ) => {
|
||||||
$(
|
$(
|
||||||
@@ -135,7 +134,7 @@ macro_rules! impl_from_integer_for_windows_result {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from_integer_for_windows_result!(isize, u32, i32);
|
impl_from_integer_for_windows_result!(usize, isize, u16, u32, i32);
|
||||||
|
|
||||||
impl<T, E> From<WindowsResult<T, E>> for Result<T, E> {
|
impl<T, E> From<WindowsResult<T, E>> for Result<T, E> {
|
||||||
fn from(result: WindowsResult<T, E>) -> Self {
|
fn from(result: WindowsResult<T, E>) -> Self {
|
||||||
@@ -146,6 +145,41 @@ impl<T, E> From<WindowsResult<T, E>> for Result<T, E> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ProcessWindowsCrateResult<T> {
|
||||||
|
fn process(self) -> Result<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_process_windows_crate_integer_wrapper_result {
|
||||||
|
( $($input:ty => $deref:ty),+ $(,)? ) => (
|
||||||
|
paste::paste! {
|
||||||
|
$(
|
||||||
|
impl ProcessWindowsCrateResult<$deref> for $input {
|
||||||
|
fn process(self) -> Result<$deref> {
|
||||||
|
if self == $input(0) {
|
||||||
|
Err(std::io::Error::last_os_error().into())
|
||||||
|
} else {
|
||||||
|
Ok(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_process_windows_crate_integer_wrapper_result!(
|
||||||
|
HWND => isize,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<T> ProcessWindowsCrateResult<T> for WindowsCrateResult<T> {
|
||||||
|
fn process(self) -> Result<T> {
|
||||||
|
match self {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(error) => Err(error.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct WindowsApi;
|
pub struct WindowsApi;
|
||||||
|
|
||||||
impl WindowsApi {
|
impl WindowsApi {
|
||||||
@@ -153,37 +187,49 @@ impl WindowsApi {
|
|||||||
callback: MONITORENUMPROC,
|
callback: MONITORENUMPROC,
|
||||||
callback_data_address: isize,
|
callback_data_address: isize,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
Result::from(WindowsResult::from(unsafe {
|
unsafe {
|
||||||
EnumDisplayMonitors(
|
EnumDisplayMonitors(
|
||||||
HDC(0),
|
HDC(0),
|
||||||
std::ptr::null_mut(),
|
std::ptr::null_mut(),
|
||||||
Option::from(callback),
|
callback,
|
||||||
LPARAM(callback_data_address),
|
LPARAM(callback_data_address),
|
||||||
)
|
)
|
||||||
}))
|
}
|
||||||
|
.ok()
|
||||||
|
.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn valid_hmonitors() -> Result<Vec<isize>> {
|
||||||
|
let mut monitors: Vec<isize> = vec![];
|
||||||
|
let monitors_ref: &mut Vec<isize> = monitors.as_mut();
|
||||||
|
Self::enum_display_monitors(
|
||||||
|
Option::Some(windows_callbacks::valid_display_monitors),
|
||||||
|
monitors_ref as *mut Vec<isize> as isize,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(monitors)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
|
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
|
||||||
Self::enum_display_monitors(
|
Self::enum_display_monitors(
|
||||||
windows_callbacks::enum_display_monitor,
|
Option::Some(windows_callbacks::enum_display_monitor),
|
||||||
monitors as *mut Ring<Monitor> as isize,
|
monitors as *mut Ring<Monitor> as isize,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
|
pub fn enum_windows(callback: WNDENUMPROC, callback_data_address: isize) -> Result<()> {
|
||||||
Result::from(WindowsResult::from(unsafe {
|
unsafe { EnumWindows(callback, LPARAM(callback_data_address)) }
|
||||||
EnumWindows(Option::from(callback), LPARAM(callback_data_address))
|
.ok()
|
||||||
}))
|
.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_workspace_information(monitors: &mut Ring<Monitor>) -> Result<()> {
|
pub fn load_workspace_information(monitors: &mut Ring<Monitor>) -> Result<()> {
|
||||||
for monitor in monitors.elements_mut() {
|
for monitor in monitors.elements_mut() {
|
||||||
if monitor.workspaces().is_empty() {
|
let monitor_id = monitor.id();
|
||||||
let mut workspace = Workspace::default();
|
if let Some(workspace) = monitor.workspaces_mut().front_mut() {
|
||||||
|
|
||||||
// EnumWindows will enumerate through windows on all monitors
|
// EnumWindows will enumerate through windows on all monitors
|
||||||
Self::enum_windows(
|
Self::enum_windows(
|
||||||
windows_callbacks::enum_window,
|
Option::Some(windows_callbacks::enum_window),
|
||||||
workspace.containers_mut() as *mut VecDeque<Container> as isize,
|
workspace.containers_mut() as *mut VecDeque<Container> as isize,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -197,7 +243,7 @@ impl WindowsApi {
|
|||||||
|
|
||||||
for container in workspace.containers_mut() {
|
for container in workspace.containers_mut() {
|
||||||
for window in container.windows() {
|
for window in container.windows() {
|
||||||
if Self::monitor_from_window(window.hwnd()) != monitor.id() {
|
if Self::monitor_from_window(window.hwnd()) != monitor_id {
|
||||||
windows_on_other_monitors.push(window.hwnd().0);
|
windows_on_other_monitors.push(window.hwnd().0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,8 +252,6 @@ impl WindowsApi {
|
|||||||
for hwnd in windows_on_other_monitors {
|
for hwnd in windows_on_other_monitors {
|
||||||
workspace.remove_window(hwnd)?;
|
workspace.remove_window(hwnd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor.workspaces_mut().push_back(workspace);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,9 +259,9 @@ impl WindowsApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn allow_set_foreground_window(process_id: u32) -> Result<()> {
|
pub fn allow_set_foreground_window(process_id: u32) -> Result<()> {
|
||||||
Result::from(WindowsResult::from(unsafe {
|
unsafe { AllowSetForegroundWindow(process_id) }
|
||||||
AllowSetForegroundWindow(process_id)
|
.ok()
|
||||||
}))
|
.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn monitor_from_window(hwnd: HWND) -> isize {
|
pub fn monitor_from_window(hwnd: HWND) -> isize {
|
||||||
@@ -226,6 +270,12 @@ impl WindowsApi {
|
|||||||
unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0
|
unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn monitor_from_point(point: POINT) -> isize {
|
||||||
|
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
|
||||||
|
unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0
|
||||||
|
}
|
||||||
|
|
||||||
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
|
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
|
||||||
let flags = SetWindowPosition::NO_ACTIVATE;
|
let flags = SetWindowPosition::NO_ACTIVATE;
|
||||||
|
|
||||||
@@ -233,8 +283,26 @@ impl WindowsApi {
|
|||||||
Self::set_window_pos(hwnd, layout, position, flags.bits())
|
Self::set_window_pos(hwnd, layout, position, flags.bits())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn position_border_window(hwnd: HWND, layout: &Rect, activate: bool) -> Result<()> {
|
||||||
|
let flags = if activate {
|
||||||
|
SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE
|
||||||
|
} else {
|
||||||
|
SetWindowPosition::NO_ACTIVATE
|
||||||
|
};
|
||||||
|
|
||||||
|
let position = HWND_BOTTOM;
|
||||||
|
Self::set_window_pos(hwnd, layout, position, flags.bits())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide_border_window(hwnd: HWND) -> Result<()> {
|
||||||
|
let flags = SetWindowPosition::HIDE_WINDOW;
|
||||||
|
|
||||||
|
let position = HWND_BOTTOM;
|
||||||
|
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
|
pub fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
|
||||||
Result::from(WindowsResult::from(unsafe {
|
unsafe {
|
||||||
SetWindowPos(
|
SetWindowPos(
|
||||||
hwnd,
|
hwnd,
|
||||||
position,
|
position,
|
||||||
@@ -244,7 +312,9 @@ impl WindowsApi {
|
|||||||
layout.bottom,
|
layout.bottom,
|
||||||
SET_WINDOW_POS_FLAGS(flags),
|
SET_WINDOW_POS_FLAGS(flags),
|
||||||
)
|
)
|
||||||
}))
|
}
|
||||||
|
.ok()
|
||||||
|
.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
|
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
|
||||||
@@ -253,12 +323,20 @@ impl WindowsApi {
|
|||||||
unsafe { ShowWindow(hwnd, command) };
|
unsafe { ShowWindow(hwnd, command) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn minimize_window(hwnd: HWND) {
|
||||||
|
Self::show_window(hwnd, SW_MINIMIZE);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn hide_window(hwnd: HWND) {
|
pub fn hide_window(hwnd: HWND) {
|
||||||
Self::show_window(hwnd, SW_HIDE);
|
Self::show_window(hwnd, SW_HIDE);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore_window(hwnd: HWND) {
|
pub fn restore_window(hwnd: HWND) {
|
||||||
Self::show_window(hwnd, SW_RESTORE);
|
Self::show_window(hwnd, SW_SHOWNOACTIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmaximize_window(hwnd: HWND) {
|
||||||
|
Self::show_window(hwnd, SW_NORMAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn maximize_window(hwnd: HWND) {
|
pub fn maximize_window(hwnd: HWND) {
|
||||||
@@ -266,39 +344,25 @@ impl WindowsApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn foreground_window() -> Result<isize> {
|
pub fn foreground_window() -> Result<isize> {
|
||||||
Result::from(WindowsResult::from(unsafe { GetForegroundWindow() }))
|
unsafe { GetForegroundWindow() }.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
|
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
|
||||||
match WindowsResult::from(unsafe { SetForegroundWindow(hwnd) }) {
|
unsafe { SetForegroundWindow(hwnd) }.ok().process()
|
||||||
WindowsResult::Ok(_) => Ok(()),
|
|
||||||
WindowsResult::Err(error) => {
|
|
||||||
// TODO: Figure out the odd behaviour here, docs state that a zero value means
|
|
||||||
// TODO: that the window was not brought to the foreground, but this contradicts
|
|
||||||
// TODO: the behaviour that I have observed which resulted in this check
|
|
||||||
if error.to_string() == "The operation completed successfully. (os error 0)" {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn top_window() -> Result<isize> {
|
pub fn top_window() -> Result<isize> {
|
||||||
Result::from(WindowsResult::from(unsafe { GetTopWindow(HWND::NULL).0 }))
|
unsafe { GetTopWindow(HWND::default()) }.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn desktop_window() -> Result<isize> {
|
pub fn desktop_window() -> Result<isize> {
|
||||||
Result::from(WindowsResult::from(unsafe { GetDesktopWindow() }))
|
unsafe { GetDesktopWindow() }.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn next_window(hwnd: HWND) -> Result<isize> {
|
pub fn next_window(hwnd: HWND) -> Result<isize> {
|
||||||
Result::from(WindowsResult::from(unsafe {
|
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.process()
|
||||||
GetWindow(hwnd, GW_HWNDNEXT).0
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -314,33 +378,35 @@ impl WindowsApi {
|
|||||||
next_hwnd = Self::next_window(HWND(next_hwnd))?;
|
next_hwnd = Self::next_window(HWND(next_hwnd))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(eyre::anyhow!("could not find next window"))
|
Err(anyhow!("could not find next window"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
|
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
|
||||||
let mut rect = unsafe { std::mem::zeroed() };
|
let mut rect = unsafe { std::mem::zeroed() };
|
||||||
|
unsafe { GetWindowRect(hwnd, &mut rect) }.ok().process()?;
|
||||||
Result::from(WindowsResult::from(unsafe {
|
|
||||||
GetWindowRect(hwnd, &mut rect)
|
|
||||||
}))?;
|
|
||||||
|
|
||||||
Ok(Rect::from(rect))
|
Ok(Rect::from(rect))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
|
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
|
||||||
Result::from(WindowsResult::from(unsafe { SetCursorPos(x, y) }))
|
unsafe { SetCursorPos(x, y) }.ok().process()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor_pos() -> Result<POINT> {
|
pub fn cursor_pos() -> Result<POINT> {
|
||||||
let mut cursor_pos: POINT = unsafe { std::mem::zeroed() };
|
let mut cursor_pos = POINT::default();
|
||||||
|
unsafe { GetCursorPos(&mut cursor_pos) }.ok().process()?;
|
||||||
Result::from(WindowsResult::from(unsafe {
|
|
||||||
GetCursorPos(&mut cursor_pos)
|
|
||||||
}))?;
|
|
||||||
|
|
||||||
Ok(cursor_pos)
|
Ok(cursor_pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn window_from_point(point: POINT) -> Result<isize> {
|
||||||
|
unsafe { WindowFromPoint(point) }.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_at_cursor_pos() -> Result<isize> {
|
||||||
|
Self::window_from_point(Self::cursor_pos()?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn center_cursor_in_rect(rect: &Rect) -> Result<()> {
|
pub fn center_cursor_in_rect(rect: &Rect) -> Result<()> {
|
||||||
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
|
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
|
||||||
}
|
}
|
||||||
@@ -363,25 +429,27 @@ impl WindowsApi {
|
|||||||
unsafe { GetCurrentProcessId() }
|
unsafe { GetCurrentProcessId() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn process_id_to_session_id() -> Result<u32> {
|
||||||
|
let process_id = Self::current_process_id();
|
||||||
|
let mut session_id = 0;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
if ProcessIdToSessionId(process_id, &mut session_id).as_bool() {
|
||||||
|
Ok(session_id)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("could not determine current session id"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> {
|
pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> {
|
||||||
Result::from(WindowsResult::from(unsafe {
|
unsafe { AttachThreadInput(thread_id, target_thread_id, attach) }
|
||||||
AttachThreadInput(thread_id, target_thread_id, attach)
|
.ok()
|
||||||
}))
|
.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_focus(hwnd: HWND) -> Result<()> {
|
pub fn set_focus(hwnd: HWND) -> Result<()> {
|
||||||
match WindowsResult::from(unsafe { SetFocus(hwnd) }) {
|
unsafe { SetFocus(hwnd) }.process().map(|_| ())
|
||||||
WindowsResult::Ok(_) => Ok(()),
|
|
||||||
WindowsResult::Err(error) => {
|
|
||||||
// If the window is not attached to the calling thread's message queue, the return value is NULL
|
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setfocus
|
|
||||||
if error.to_string() == "The operation completed successfully. (os error 0)" {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -415,11 +483,14 @@ impl WindowsApi {
|
|||||||
Self::set_window_long_ptr_w(hwnd, GWL_STYLE, new_value)
|
Self::set_window_long_ptr_w(hwnd, GWL_STYLE, new_value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn update_ex_style(hwnd: HWND, new_value: isize) -> Result<()> {
|
||||||
|
Self::set_window_long_ptr_w(hwnd, GWL_EXSTYLE, new_value)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn window_text_w(hwnd: HWND) -> Result<String> {
|
pub fn window_text_w(hwnd: HWND) -> Result<String> {
|
||||||
let mut text: [u16; 512] = [0; 512];
|
let mut text: [u16; 512] = [0; 512];
|
||||||
match WindowsResult::from(unsafe {
|
match WindowsResult::from(unsafe { GetWindowTextW(hwnd, &mut text) }) {
|
||||||
GetWindowTextW(hwnd, PWSTR(text.as_mut_ptr()), text.len().try_into()?)
|
|
||||||
}) {
|
|
||||||
WindowsResult::Ok(len) => {
|
WindowsResult::Ok(len) => {
|
||||||
let length = usize::try_from(len)?;
|
let length = usize::try_from(len)?;
|
||||||
Ok(String::from_utf16(&text[..length])?)
|
Ok(String::from_utf16(&text[..length])?)
|
||||||
@@ -433,9 +504,7 @@ impl WindowsApi {
|
|||||||
inherit_handle: bool,
|
inherit_handle: bool,
|
||||||
process_id: u32,
|
process_id: u32,
|
||||||
) -> Result<HANDLE> {
|
) -> Result<HANDLE> {
|
||||||
Result::from(WindowsResult::from(unsafe {
|
unsafe { OpenProcess(access_rights, inherit_handle, process_id) }.process()
|
||||||
OpenProcess(access_rights, inherit_handle, process_id)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
|
pub fn process_handle(process_id: u32) -> Result<HANDLE> {
|
||||||
@@ -447,14 +516,16 @@ impl WindowsApi {
|
|||||||
let mut path: Vec<u16> = vec![0; len as usize];
|
let mut path: Vec<u16> = vec![0; len as usize];
|
||||||
let text_ptr = path.as_mut_ptr();
|
let text_ptr = path.as_mut_ptr();
|
||||||
|
|
||||||
Result::from(WindowsResult::from(unsafe {
|
unsafe {
|
||||||
QueryFullProcessImageNameW(
|
QueryFullProcessImageNameW(
|
||||||
handle,
|
handle,
|
||||||
PROCESS_NAME_FORMAT(0),
|
PROCESS_NAME_WIN32,
|
||||||
PWSTR(text_ptr),
|
PWSTR(text_ptr),
|
||||||
&mut len as *mut u32,
|
std::ptr::addr_of_mut!(len),
|
||||||
)
|
)
|
||||||
}))?;
|
}
|
||||||
|
.ok()
|
||||||
|
.process()?;
|
||||||
|
|
||||||
Ok(String::from_utf16(&path[..len as usize])?)
|
Ok(String::from_utf16(&path[..len as usize])?)
|
||||||
}
|
}
|
||||||
@@ -463,7 +534,7 @@ impl WindowsApi {
|
|||||||
Ok(Self::exe_path(handle)?
|
Ok(Self::exe_path(handle)?
|
||||||
.split('\\')
|
.split('\\')
|
||||||
.last()
|
.last()
|
||||||
.context("there is no last element")?
|
.ok_or_else(|| anyhow!("there is no last element"))?
|
||||||
.to_string())
|
.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +543,7 @@ impl WindowsApi {
|
|||||||
let mut class: [u16; BUF_SIZE] = [0; BUF_SIZE];
|
let mut class: [u16; BUF_SIZE] = [0; BUF_SIZE];
|
||||||
|
|
||||||
let len = Result::from(WindowsResult::from(unsafe {
|
let len = Result::from(WindowsResult::from(unsafe {
|
||||||
RealGetWindowClassW(hwnd, PWSTR(class.as_mut_ptr()), u32::try_from(BUF_SIZE)?)
|
RealGetWindowClassW(hwnd, &mut class)
|
||||||
}))?;
|
}))?;
|
||||||
|
|
||||||
Ok(String::from_utf16(&class[0..len as usize])?)
|
Ok(String::from_utf16(&class[0..len as usize])?)
|
||||||
@@ -486,7 +557,7 @@ impl WindowsApi {
|
|||||||
unsafe {
|
unsafe {
|
||||||
DwmGetWindowAttribute(
|
DwmGetWindowAttribute(
|
||||||
hwnd,
|
hwnd,
|
||||||
std::mem::transmute::<_, u32>(attribute),
|
attribute,
|
||||||
(value as *mut T).cast(),
|
(value as *mut T).cast(),
|
||||||
u32::try_from(std::mem::size_of::<T>())?,
|
u32::try_from(std::mem::size_of::<T>())?,
|
||||||
)?;
|
)?;
|
||||||
@@ -529,34 +600,56 @@ impl WindowsApi {
|
|||||||
let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() };
|
let mut monitor_info: MONITORINFO = unsafe { std::mem::zeroed() };
|
||||||
monitor_info.cbSize = u32::try_from(std::mem::size_of::<MONITORINFO>())?;
|
monitor_info.cbSize = u32::try_from(std::mem::size_of::<MONITORINFO>())?;
|
||||||
|
|
||||||
Result::from(WindowsResult::from(unsafe {
|
unsafe { GetMonitorInfoW(hmonitor, std::ptr::addr_of_mut!(monitor_info).cast()) }
|
||||||
GetMonitorInfoW(hmonitor, (&mut monitor_info as *mut MONITORINFO).cast())
|
.ok()
|
||||||
}))?;
|
.process()?;
|
||||||
|
|
||||||
Ok(monitor_info)
|
Ok(monitor_info)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn monitor(hmonitor: HMONITOR) -> Result<Monitor> {
|
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
|
||||||
let monitor_info = Self::monitor_info_w(hmonitor)?;
|
let monitor_info = Self::monitor_info_w(HMONITOR(hmonitor))?;
|
||||||
|
|
||||||
Ok(monitor::new(
|
Ok(monitor::new(
|
||||||
hmonitor.0,
|
hmonitor,
|
||||||
monitor_info.rcMonitor.into(),
|
monitor_info.rcMonitor.into(),
|
||||||
monitor_info.rcWork.into(),
|
monitor_info.rcWork.into(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_process_dpi_awareness_context() -> Result<()> {
|
||||||
|
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }
|
||||||
|
.ok()
|
||||||
|
.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn system_parameters_info_w(
|
pub fn system_parameters_info_w(
|
||||||
action: SYSTEM_PARAMETERS_INFO_ACTION,
|
action: SYSTEM_PARAMETERS_INFO_ACTION,
|
||||||
ui_param: u32,
|
ui_param: u32,
|
||||||
pv_param: *mut c_void,
|
pv_param: *mut c_void,
|
||||||
update_flags: SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
|
update_flags: SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
Result::from(WindowsResult::from(unsafe {
|
unsafe { SystemParametersInfoW(action, ui_param, pv_param, update_flags) }
|
||||||
SystemParametersInfoW(action, ui_param, pv_param, update_flags)
|
.ok()
|
||||||
}))
|
.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn focus_follows_mouse() -> Result<bool> {
|
||||||
|
let mut is_enabled: BOOL = unsafe { std::mem::zeroed() };
|
||||||
|
|
||||||
|
Self::system_parameters_info_w(
|
||||||
|
SPI_GETACTIVEWINDOWTRACKING,
|
||||||
|
0,
|
||||||
|
std::ptr::addr_of_mut!(is_enabled).cast(),
|
||||||
|
SPIF_SENDCHANGE,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(is_enabled.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn enable_focus_follows_mouse() -> Result<()> {
|
pub fn enable_focus_follows_mouse() -> Result<()> {
|
||||||
Self::system_parameters_info_w(
|
Self::system_parameters_info_w(
|
||||||
SPI_SETACTIVEWINDOWTRACKING,
|
SPI_SETACTIVEWINDOWTRACKING,
|
||||||
@@ -566,6 +659,7 @@ impl WindowsApi {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn disable_focus_follows_mouse() -> Result<()> {
|
pub fn disable_focus_follows_mouse() -> Result<()> {
|
||||||
Self::system_parameters_info_w(
|
Self::system_parameters_info_w(
|
||||||
SPI_SETACTIVEWINDOWTRACKING,
|
SPI_SETACTIVEWINDOWTRACKING,
|
||||||
@@ -574,4 +668,71 @@ impl WindowsApi {
|
|||||||
SPIF_SENDCHANGE,
|
SPIF_SENDCHANGE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn module_handle_w() -> Result<HINSTANCE> {
|
||||||
|
unsafe { GetModuleHandleW(None) }.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_solid_brush(r: u32, g: u32, b: u32) -> HBRUSH {
|
||||||
|
unsafe { CreateSolidBrush(r | (g << 8) | (b << 16)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_class_a(window_class: &WNDCLASSA) -> Result<u16> {
|
||||||
|
Result::from(WindowsResult::from(unsafe { RegisterClassA(window_class) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scale_factor_for_monitor(hmonitor: isize) -> Result<DEVICE_SCALE_FACTOR> {
|
||||||
|
unsafe { GetScaleFactorForMonitor(HMONITOR(hmonitor)) }.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn monitors_have_same_scale_factor(a: isize, b: isize) -> Result<bool> {
|
||||||
|
let a = Self::scale_factor_for_monitor(a)?;
|
||||||
|
let b = Self::scale_factor_for_monitor(b)?;
|
||||||
|
|
||||||
|
Ok(a == b)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn change_border_colour(hwnd: isize, r: u32, g: u32, b: u32) -> Result<usize> {
|
||||||
|
Result::from(WindowsResult::from(unsafe {
|
||||||
|
SetClassLongPtrW(
|
||||||
|
HWND(hwnd),
|
||||||
|
GCLP_HBRBACKGROUND,
|
||||||
|
Self::create_solid_brush(r, g, b).0,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn round_corners(hwnd: isize) -> Result<()> {
|
||||||
|
let round = DWMWCP_ROUND;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
DwmSetWindowAttribute(
|
||||||
|
HWND(hwnd),
|
||||||
|
DWMWA_WINDOW_CORNER_PREFERENCE,
|
||||||
|
std::ptr::addr_of!(round).cast(),
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_border_window(name: PCSTR, instance: HINSTANCE) -> Result<isize> {
|
||||||
|
unsafe {
|
||||||
|
CreateWindowExA(
|
||||||
|
WS_EX_TOOLWINDOW,
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
WS_POPUP | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
instance,
|
||||||
|
std::ptr::null(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.process()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use bindings::Windows::Win32::Foundation::BOOL;
|
use windows::Win32::Foundation::BOOL;
|
||||||
use bindings::Windows::Win32::Foundation::HWND;
|
use windows::Win32::Foundation::HWND;
|
||||||
use bindings::Windows::Win32::Foundation::LPARAM;
|
use windows::Win32::Foundation::LPARAM;
|
||||||
use bindings::Windows::Win32::Foundation::RECT;
|
use windows::Win32::Foundation::LRESULT;
|
||||||
use bindings::Windows::Win32::Graphics::Gdi::HDC;
|
use windows::Win32::Foundation::RECT;
|
||||||
use bindings::Windows::Win32::Graphics::Gdi::HMONITOR;
|
use windows::Win32::Foundation::WPARAM;
|
||||||
use bindings::Windows::Win32::UI::Accessibility::HWINEVENTHOOK;
|
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||||
|
use windows::Win32::Graphics::Gdi::HDC;
|
||||||
|
use windows::Win32::Graphics::Gdi::HMONITOR;
|
||||||
|
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||||
|
|
||||||
use crate::container::Container;
|
use crate::container::Container;
|
||||||
use crate::monitor::Monitor;
|
use crate::monitor::Monitor;
|
||||||
@@ -16,6 +23,17 @@ use crate::window_manager_event::WindowManagerEvent;
|
|||||||
use crate::windows_api::WindowsApi;
|
use crate::windows_api::WindowsApi;
|
||||||
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
|
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
|
||||||
|
|
||||||
|
pub extern "system" fn valid_display_monitors(
|
||||||
|
hmonitor: HMONITOR,
|
||||||
|
_: HDC,
|
||||||
|
_: *mut RECT,
|
||||||
|
lparam: LPARAM,
|
||||||
|
) -> BOOL {
|
||||||
|
let monitors = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||||
|
monitors.push(hmonitor.0);
|
||||||
|
true.into()
|
||||||
|
}
|
||||||
|
|
||||||
pub extern "system" fn enum_display_monitor(
|
pub extern "system" fn enum_display_monitor(
|
||||||
hmonitor: HMONITOR,
|
hmonitor: HMONITOR,
|
||||||
_: HDC,
|
_: HDC,
|
||||||
@@ -23,7 +41,15 @@ pub extern "system" fn enum_display_monitor(
|
|||||||
lparam: LPARAM,
|
lparam: LPARAM,
|
||||||
) -> BOOL {
|
) -> BOOL {
|
||||||
let monitors = unsafe { &mut *(lparam.0 as *mut Ring<Monitor>) };
|
let monitors = unsafe { &mut *(lparam.0 as *mut Ring<Monitor>) };
|
||||||
if let Ok(m) = WindowsApi::monitor(hmonitor) {
|
|
||||||
|
// Don't duplicate a monitor that is already being managed
|
||||||
|
for monitor in monitors.elements() {
|
||||||
|
if monitor.id() == hmonitor.0 {
|
||||||
|
return true.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(m) = WindowsApi::monitor(hmonitor.0) {
|
||||||
monitors.elements_mut().push_back(m);
|
monitors.elements_mut().push_back(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,3 +110,24 @@ pub extern "system" fn win_event_hook(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub extern "system" fn border_window(
|
||||||
|
window: HWND,
|
||||||
|
message: u32,
|
||||||
|
wparam: WPARAM,
|
||||||
|
lparam: LPARAM,
|
||||||
|
) -> LRESULT {
|
||||||
|
unsafe {
|
||||||
|
match message as u32 {
|
||||||
|
WM_PAINT => {
|
||||||
|
InvalidateRect(window, std::ptr::null(), true);
|
||||||
|
LRESULT(0)
|
||||||
|
}
|
||||||
|
WM_DESTROY => {
|
||||||
|
PostQuitMessage(0);
|
||||||
|
LRESULT(0)
|
||||||
|
}
|
||||||
|
_ => DefWindowProcW(window, message, wparam, lparam),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,91 +1,94 @@
|
|||||||
|
#![allow(clippy::use_self)]
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Serialize;
|
||||||
use strum::Display;
|
use strum::Display;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_START;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_CARET;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END_APPLICATION;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_LAYOUT;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_START_APPLICATION;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_REGION;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SCROLL;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SIMPLE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_ACCELERATORCHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CLOAKED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CONTENTSCROLLED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CREATE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DEFACTIONCHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESCRIPTIONCHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCANCEL;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCOMPLETE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGDROPPED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGENTER;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGLEAVE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGSTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_END;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_FOCUS;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HELPCHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HIDE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_CHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_HIDE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_SHOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_INVOKED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LIVEREGIONCHANGED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_NAMECHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_PARENTCHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_REORDER;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTION;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONADD;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONREMOVE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONWITHIN;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SHOW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_STATECHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTSELECTIONCHANGED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_UNCLOAKED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_VALUECHANGE;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_END;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_START;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ALERT;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ARRANGMENTPREVIEW;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTUREEND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTURESTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPEND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPSTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DESKTOPSWITCH;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGEND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGSTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPEND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPSTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_END;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_FOREGROUND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_IME_KEY_NOTIFICATION;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUEND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPEND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPSTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUSTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZEEND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZESTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZEEND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZESTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGEND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGSTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SOUND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHEND;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPDROPPED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPGRABBED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPOVERTARGET;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_CANCELLED;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHSTART;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_END;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
|
||||||
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
|
||||||
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Display, JsonSchema)]
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_START;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_CARET;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_END_APPLICATION;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_LAYOUT;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_START_APPLICATION;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_REGION;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SCROLL;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_CONSOLE_UPDATE_SIMPLE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_ACCELERATORCHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CLOAKED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CONTENTSCROLLED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_CREATE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DEFACTIONCHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESCRIPTIONCHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCANCEL;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGCOMPLETE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGDROPPED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGENTER;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGLEAVE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DRAGSTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_END;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_FOCUS;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HELPCHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HIDE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_CHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_HIDE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_IME_SHOW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_INVOKED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LIVEREGIONCHANGED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_NAMECHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_PARENTCHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_REORDER;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTION;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONADD;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONREMOVE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SELECTIONWITHIN;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_SHOW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_STATECHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_TEXTSELECTIONCHANGED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_UNCLOAKED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_VALUECHANGE;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_END;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_OEM_DEFINED_START;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ALERT;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_ARRANGMENTPREVIEW;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTUREEND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CAPTURESTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPEND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_CONTEXTHELPSTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DESKTOPSWITCH;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGEND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DIALOGSTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPEND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_DRAGDROPSTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_END;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_FOREGROUND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_IME_KEY_NOTIFICATION;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUEND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPEND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUPOPUPSTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MENUSTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZEEND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MINIMIZESTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZEEND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_MOVESIZESTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGEND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SCROLLINGSTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SOUND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHEND;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPDROPPED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPGRABBED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_APPOVERTARGET;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHER_CANCELLED;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_SYSTEM_SWITCHSTART;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_END;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
|
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Debug, Display)]
|
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum WinEvent {
|
pub enum WinEvent {
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
use std::sync::atomic::AtomicIsize;
|
use std::sync::atomic::AtomicIsize;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use windows::Win32::Foundation::HWND;
|
||||||
use bindings::Windows::Win32::Foundation::HWND;
|
use windows::Win32::UI::Accessibility::SetWinEventHook;
|
||||||
use bindings::Windows::Win32::UI::Accessibility::SetWinEventHook;
|
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
use windows::Win32::UI::WindowsAndMessaging::PeekMessageW;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::PeekMessageW;
|
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_MAX;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_MAX;
|
use windows::Win32::UI::WindowsAndMessaging::EVENT_MIN;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::EVENT_MIN;
|
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::MSG;
|
use windows::Win32::UI::WindowsAndMessaging::PM_REMOVE;
|
||||||
use bindings::Windows::Win32::UI::WindowsAndMessaging::PM_REMOVE;
|
|
||||||
|
|
||||||
use crate::window_manager_event::WindowManagerEvent;
|
use crate::window_manager_event::WindowManagerEvent;
|
||||||
use crate::windows_callbacks;
|
use crate::windows_callbacks;
|
||||||
@@ -45,10 +43,10 @@ impl WinEventListener {
|
|||||||
let hook = self.hook.clone();
|
let hook = self.hook.clone();
|
||||||
let outgoing = self.outgoing_events.lock().clone();
|
let outgoing = self.outgoing_events.lock().clone();
|
||||||
|
|
||||||
thread::spawn(move || unsafe {
|
std::thread::spawn(move || unsafe {
|
||||||
let hook_ref = SetWinEventHook(
|
let hook_ref = SetWinEventHook(
|
||||||
EVENT_MIN as u32,
|
EVENT_MIN,
|
||||||
EVENT_MAX as u32,
|
EVENT_MAX,
|
||||||
None,
|
None,
|
||||||
Some(windows_callbacks::win_event_hook),
|
Some(windows_callbacks::win_event_hook),
|
||||||
0,
|
0,
|
||||||
@@ -97,7 +95,7 @@ impl MessageLoop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thread::sleep(Duration::from_millis(sleep));
|
std::thread::sleep(Duration::from_millis(sleep));
|
||||||
|
|
||||||
if !cb(value) {
|
if !cb(value) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
use color_eyre::eyre::ContextCompat;
|
use color_eyre::eyre::anyhow;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use getset::CopyGetters;
|
use getset::CopyGetters;
|
||||||
use getset::Getters;
|
use getset::Getters;
|
||||||
use getset::MutGetters;
|
use getset::MutGetters;
|
||||||
use getset::Setters;
|
use getset::Setters;
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use komorebi_core::Flip;
|
use komorebi_core::Axis;
|
||||||
|
use komorebi_core::CycleDirection;
|
||||||
|
use komorebi_core::DefaultLayout;
|
||||||
use komorebi_core::Layout;
|
use komorebi_core::Layout;
|
||||||
use komorebi_core::OperationDirection;
|
use komorebi_core::OperationDirection;
|
||||||
use komorebi_core::Rect;
|
use komorebi_core::Rect;
|
||||||
@@ -18,8 +22,9 @@ use crate::container::Container;
|
|||||||
use crate::ring::Ring;
|
use crate::ring::Ring;
|
||||||
use crate::window::Window;
|
use crate::window::Window;
|
||||||
use crate::windows_api::WindowsApi;
|
use crate::windows_api::WindowsApi;
|
||||||
|
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters)]
|
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
#[getset(set = "pub")]
|
#[getset(set = "pub")]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
@@ -36,10 +41,12 @@ pub struct Workspace {
|
|||||||
maximized_window_restore_idx: Option<usize>,
|
maximized_window_restore_idx: Option<usize>,
|
||||||
#[getset(get = "pub", get_mut = "pub")]
|
#[getset(get = "pub", get_mut = "pub")]
|
||||||
floating_windows: Vec<Window>,
|
floating_windows: Vec<Window>,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||||
layout: Layout,
|
layout: Layout,
|
||||||
|
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||||
|
layout_rules: Vec<(usize, Layout)>,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
#[getset(get_copy = "pub", set = "pub")]
|
||||||
layout_flip: Option<Flip>,
|
layout_flip: Option<Axis>,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
#[getset(get_copy = "pub", set = "pub")]
|
||||||
workspace_padding: Option<i32>,
|
workspace_padding: Option<i32>,
|
||||||
#[getset(get_copy = "pub", set = "pub")]
|
#[getset(get_copy = "pub", set = "pub")]
|
||||||
@@ -47,8 +54,7 @@ pub struct Workspace {
|
|||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
#[getset(get = "pub", set = "pub")]
|
#[getset(get = "pub", set = "pub")]
|
||||||
latest_layout: Vec<Rect>,
|
latest_layout: Vec<Rect>,
|
||||||
#[serde(skip_serializing)]
|
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||||
#[getset(get = "pub", get_mut = "pub")]
|
|
||||||
resize_dimensions: Vec<Option<Rect>>,
|
resize_dimensions: Vec<Option<Rect>>,
|
||||||
#[getset(get = "pub", set = "pub")]
|
#[getset(get = "pub", set = "pub")]
|
||||||
tile: bool,
|
tile: bool,
|
||||||
@@ -66,7 +72,8 @@ impl Default for Workspace {
|
|||||||
maximized_window_restore_idx: None,
|
maximized_window_restore_idx: None,
|
||||||
monocle_container_restore_idx: None,
|
monocle_container_restore_idx: None,
|
||||||
floating_windows: Vec::default(),
|
floating_windows: Vec::default(),
|
||||||
layout: Layout::BSP,
|
layout: Layout::Default(DefaultLayout::BSP),
|
||||||
|
layout_rules: vec![],
|
||||||
layout_flip: None,
|
layout_flip: None,
|
||||||
workspace_padding: Option::from(10),
|
workspace_padding: Option::from(10),
|
||||||
container_padding: Option::from(10),
|
container_padding: Option::from(10),
|
||||||
@@ -100,7 +107,7 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore(&mut self) -> Result<()> {
|
pub fn restore(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
||||||
let idx = self.focused_container_idx();
|
let idx = self.focused_container_idx();
|
||||||
let mut to_focus = None;
|
let mut to_focus = None;
|
||||||
for (i, container) in self.containers_mut().iter_mut().enumerate() {
|
for (i, container) in self.containers_mut().iter_mut().enumerate() {
|
||||||
@@ -131,32 +138,75 @@ impl Workspace {
|
|||||||
// Maximised windows should always be drawn at the top of the Z order
|
// Maximised windows should always be drawn at the top of the Z order
|
||||||
if let Some(window) = to_focus {
|
if let Some(window) = to_focus {
|
||||||
if self.maximized_window().is_none() {
|
if self.maximized_window().is_none() {
|
||||||
window.focus()?;
|
window.focus(mouse_follows_focus)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, work_area: &Rect) -> Result<()> {
|
pub fn update(
|
||||||
let mut adjusted_work_area = *work_area;
|
&mut self,
|
||||||
|
work_area: &Rect,
|
||||||
|
offset: Option<Rect>,
|
||||||
|
invisible_borders: &Rect,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let container_padding = self.container_padding();
|
||||||
|
let mut adjusted_work_area = offset.map_or_else(
|
||||||
|
|| *work_area,
|
||||||
|
|offset| {
|
||||||
|
let mut with_offset = *work_area;
|
||||||
|
with_offset.left += offset.left;
|
||||||
|
with_offset.top += offset.top;
|
||||||
|
with_offset.right -= offset.right;
|
||||||
|
with_offset.bottom -= offset.bottom;
|
||||||
|
|
||||||
|
with_offset
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
adjusted_work_area.add_padding(self.workspace_padding());
|
adjusted_work_area.add_padding(self.workspace_padding());
|
||||||
|
|
||||||
self.enforce_resize_constraints();
|
self.enforce_resize_constraints();
|
||||||
|
|
||||||
|
if !self.layout_rules().is_empty() {
|
||||||
|
let mut updated_layout = None;
|
||||||
|
|
||||||
|
for rule in self.layout_rules() {
|
||||||
|
if self.containers().len() >= rule.0 {
|
||||||
|
updated_layout = Option::from(rule.1.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(updated_layout) = updated_layout {
|
||||||
|
if !matches!(updated_layout, Layout::Default(DefaultLayout::BSP)) {
|
||||||
|
self.set_layout_flip(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_layout(updated_layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if *self.tile() {
|
if *self.tile() {
|
||||||
if let Some(container) = self.monocle_container_mut() {
|
if let Some(container) = self.monocle_container_mut() {
|
||||||
if let Some(window) = container.focused_window_mut() {
|
if let Some(window) = container.focused_window_mut() {
|
||||||
window.set_position(&adjusted_work_area, true)?;
|
adjusted_work_area.add_padding(container_padding);
|
||||||
|
window.set_position(&adjusted_work_area, invisible_borders, true)?;
|
||||||
};
|
};
|
||||||
} else if let Some(window) = self.maximized_window_mut() {
|
} else if let Some(window) = self.maximized_window_mut() {
|
||||||
window.maximize();
|
window.maximize();
|
||||||
} else if !self.containers().is_empty() {
|
} else if !self.containers().is_empty() {
|
||||||
let layouts = self.layout().calculate(
|
let layouts = self.layout().as_boxed_arrangement().calculate(
|
||||||
&adjusted_work_area,
|
&adjusted_work_area,
|
||||||
NonZeroUsize::new(self.containers().len()).context(
|
NonZeroUsize::new(self.containers().len()).ok_or_else(|| {
|
||||||
"there must be at least one container to calculate a workspace layout",
|
anyhow!(
|
||||||
)?,
|
"there must be at least one container to calculate a workspace layout"
|
||||||
|
)
|
||||||
|
})?,
|
||||||
self.container_padding(),
|
self.container_padding(),
|
||||||
self.layout_flip(),
|
self.layout_flip(),
|
||||||
self.resize_dimensions(),
|
self.resize_dimensions(),
|
||||||
@@ -165,7 +215,7 @@ impl Workspace {
|
|||||||
let windows = self.visible_windows_mut();
|
let windows = self.visible_windows_mut();
|
||||||
for (i, window) in windows.into_iter().enumerate() {
|
for (i, window) in windows.into_iter().enumerate() {
|
||||||
if let (Some(window), Some(layout)) = (window, layouts.get(i)) {
|
if let (Some(window), Some(layout)) = (window, layouts.get(i)) {
|
||||||
window.set_position(layout, false)?;
|
window.set_position(layout, invisible_borders, false)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,16 +279,16 @@ impl Workspace {
|
|||||||
pub fn focus_container_by_window(&mut self, hwnd: isize) -> Result<()> {
|
pub fn focus_container_by_window(&mut self, hwnd: isize) -> Result<()> {
|
||||||
let container_idx = self
|
let container_idx = self
|
||||||
.container_idx_for_window(hwnd)
|
.container_idx_for_window(hwnd)
|
||||||
.context("there is no container/window")?;
|
.ok_or_else(|| anyhow!("there is no container/window"))?;
|
||||||
|
|
||||||
let container = self
|
let container = self
|
||||||
.containers_mut()
|
.containers_mut()
|
||||||
.get_mut(container_idx)
|
.get_mut(container_idx)
|
||||||
.context("there is no container")?;
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
|
|
||||||
let window_idx = container
|
let window_idx = container
|
||||||
.idx_for_window(hwnd)
|
.idx_for_window(hwnd)
|
||||||
.context("there is no window")?;
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
container.focus_window(window_idx);
|
container.focus_window(window_idx);
|
||||||
self.focus_container(container_idx);
|
self.focus_container(container_idx);
|
||||||
@@ -262,6 +312,77 @@ impl Workspace {
|
|||||||
idx
|
idx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
|
||||||
|
for container in self.containers() {
|
||||||
|
if let Some(hwnd) = container.hwnd_from_exe(exe) {
|
||||||
|
return Option::from(hwnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = self.maximized_window() {
|
||||||
|
if let Ok(window_exe) = window.exe() {
|
||||||
|
if exe == window_exe {
|
||||||
|
return Option::from(window.hwnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(container) = self.monocle_container() {
|
||||||
|
if let Some(hwnd) = container.hwnd_from_exe(exe) {
|
||||||
|
return Option::from(hwnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for window in self.floating_windows() {
|
||||||
|
if let Ok(window_exe) = window.exe() {
|
||||||
|
if exe == window_exe {
|
||||||
|
return Option::from(window.hwnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains_managed_window(&self, hwnd: isize) -> bool {
|
||||||
|
for container in self.containers() {
|
||||||
|
if container.contains_window(hwnd) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = self.maximized_window() {
|
||||||
|
if hwnd == window.hwnd {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(container) = self.monocle_container() {
|
||||||
|
if container.contains_window(hwnd) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_focused_window_monocle_or_maximized(&self) -> Result<bool> {
|
||||||
|
let hwnd = WindowsApi::foreground_window()?;
|
||||||
|
if let Some(window) = self.maximized_window() {
|
||||||
|
if hwnd == window.hwnd {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(container) = self.monocle_container() {
|
||||||
|
if container.contains_window(hwnd) {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn contains_window(&self, hwnd: isize) -> bool {
|
pub fn contains_window(&self, hwnd: isize) -> bool {
|
||||||
for container in self.containers() {
|
for container in self.containers() {
|
||||||
if container.contains_window(hwnd) {
|
if container.contains_window(hwnd) {
|
||||||
@@ -291,12 +412,24 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn promote_container(&mut self) -> Result<()> {
|
pub fn promote_container(&mut self) -> Result<()> {
|
||||||
|
let resize = self.resize_dimensions_mut().remove(0);
|
||||||
let container = self
|
let container = self
|
||||||
.remove_focused_container()
|
.remove_focused_container()
|
||||||
.context("there is no container")?;
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
self.containers_mut().push_front(container);
|
|
||||||
self.resize_dimensions_mut().insert(0, None);
|
let primary_idx = match self.layout() {
|
||||||
self.focus_container(0);
|
Layout::Default(_) => 0,
|
||||||
|
Layout::Custom(layout) => layout.first_container_idx(
|
||||||
|
layout
|
||||||
|
.primary_idx()
|
||||||
|
.ok_or_else(|| anyhow!("this custom layout does not have a primary column"))?,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.containers_mut().insert(primary_idx, container);
|
||||||
|
self.resize_dimensions_mut().insert(primary_idx, resize);
|
||||||
|
|
||||||
|
self.focus_container(primary_idx);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -306,9 +439,20 @@ impl Workspace {
|
|||||||
self.focus_last_container();
|
self.focus_last_container();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
|
pub fn insert_container_at_idx(&mut self, idx: usize, container: Container) {
|
||||||
self.resize_dimensions_mut().remove(idx);
|
self.containers_mut().insert(idx, container);
|
||||||
self.containers_mut().remove(idx)
|
}
|
||||||
|
|
||||||
|
pub fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
|
||||||
|
if idx < self.resize_dimensions().len() {
|
||||||
|
self.resize_dimensions_mut().remove(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx < self.containers().len() {
|
||||||
|
return self.containers_mut().remove(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn container_idx_for_window(&self, hwnd: isize) -> Option<usize> {
|
fn container_idx_for_window(&self, hwnd: isize) -> Option<usize> {
|
||||||
@@ -328,37 +472,67 @@ impl Workspace {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(container) = self.monocle_container_mut() {
|
||||||
|
if let Some(window_idx) = container
|
||||||
|
.windows()
|
||||||
|
.iter()
|
||||||
|
.position(|window| window.hwnd == hwnd)
|
||||||
|
{
|
||||||
|
container
|
||||||
|
.remove_window_by_idx(window_idx)
|
||||||
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
|
if container.windows().is_empty() {
|
||||||
|
self.set_monocle_container(None);
|
||||||
|
self.set_monocle_container_restore_idx(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(window) = self.maximized_window() {
|
||||||
|
if window.hwnd == hwnd {
|
||||||
|
window.unmaximize();
|
||||||
|
self.set_maximized_window(None);
|
||||||
|
self.set_maximized_window_restore_idx(None);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let container_idx = self
|
let container_idx = self
|
||||||
.container_idx_for_window(hwnd)
|
.container_idx_for_window(hwnd)
|
||||||
.context("there is no window")?;
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
let container = self
|
let container = self
|
||||||
.containers_mut()
|
.containers_mut()
|
||||||
.get_mut(container_idx)
|
.get_mut(container_idx)
|
||||||
.context("there is no container")?;
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
|
|
||||||
let window_idx = container
|
let window_idx = container
|
||||||
.windows()
|
.windows()
|
||||||
.iter()
|
.iter()
|
||||||
.position(|window| window.hwnd == hwnd)
|
.position(|window| window.hwnd == hwnd)
|
||||||
.context("there is no window")?;
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
container
|
container
|
||||||
.remove_window_by_idx(window_idx)
|
.remove_window_by_idx(window_idx)
|
||||||
.context("there is no window")?;
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
if container.windows().is_empty() {
|
if container.windows().is_empty() {
|
||||||
self.containers_mut()
|
self.containers_mut()
|
||||||
.remove(container_idx)
|
.remove(container_idx)
|
||||||
.context("there is no container")?;
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
|
|
||||||
// Whenever a container is empty, we need to remove any resize dimensions for it too
|
// Whenever a container is empty, we need to remove any resize dimensions for it too
|
||||||
if self.resize_dimensions().get(container_idx).is_some() {
|
if self.resize_dimensions().get(container_idx).is_some() {
|
||||||
self.resize_dimensions_mut().remove(container_idx);
|
self.resize_dimensions_mut().remove(container_idx);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.focus_previous_container();
|
self.focus_previous_container();
|
||||||
|
} else {
|
||||||
|
container.load_focused_window();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -371,21 +545,28 @@ impl Workspace {
|
|||||||
container
|
container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_container(&mut self, idx: usize) -> Option<Container> {
|
||||||
|
let container = self.remove_container_by_idx(idx);
|
||||||
|
self.focus_previous_container();
|
||||||
|
|
||||||
|
container
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
|
pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
|
||||||
if direction.is_valid(
|
let len = NonZeroUsize::new(self.containers().len())?;
|
||||||
self.layout(),
|
|
||||||
|
direction.destination(
|
||||||
|
self.layout().as_boxed_direction().as_ref(),
|
||||||
self.layout_flip(),
|
self.layout_flip(),
|
||||||
self.focused_container_idx(),
|
self.focused_container_idx(),
|
||||||
self.containers().len(),
|
len,
|
||||||
) {
|
)
|
||||||
Option::from(direction.new_idx(
|
}
|
||||||
self.layout(),
|
pub fn new_idx_for_cycle_direction(&self, direction: CycleDirection) -> Option<usize> {
|
||||||
self.layout_flip(),
|
Option::from(direction.next_idx(
|
||||||
self.containers.focused_idx(),
|
self.focused_container_idx(),
|
||||||
))
|
NonZeroUsize::new(self.containers().len())?,
|
||||||
} else {
|
))
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_window_to_container(&mut self, target_container_idx: usize) -> Result<()> {
|
pub fn move_window_to_container(&mut self, target_container_idx: usize) -> Result<()> {
|
||||||
@@ -393,11 +574,11 @@ impl Workspace {
|
|||||||
|
|
||||||
let container = self
|
let container = self
|
||||||
.focused_container_mut()
|
.focused_container_mut()
|
||||||
.context("there is no container")?;
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
|
|
||||||
let window = container
|
let window = container
|
||||||
.remove_focused_window()
|
.remove_focused_window()
|
||||||
.context("there is no window")?;
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
// This is a little messy
|
// This is a little messy
|
||||||
let adjusted_target_container_index = if container.windows().is_empty() {
|
let adjusted_target_container_index = if container.windows().is_empty() {
|
||||||
@@ -417,13 +598,13 @@ impl Workspace {
|
|||||||
let target_container = self
|
let target_container = self
|
||||||
.containers_mut()
|
.containers_mut()
|
||||||
.get_mut(adjusted_target_container_index)
|
.get_mut(adjusted_target_container_index)
|
||||||
.context("there is no container")?;
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
|
|
||||||
target_container.add_window(window);
|
target_container.add_window(window);
|
||||||
|
|
||||||
self.focus_container(adjusted_target_container_index);
|
self.focus_container(adjusted_target_container_index);
|
||||||
self.focused_container_mut()
|
self.focused_container_mut()
|
||||||
.context("there is no container")?
|
.ok_or_else(|| anyhow!("there is no container"))?
|
||||||
.load_focused_window();
|
.load_focused_window();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -434,11 +615,11 @@ impl Workspace {
|
|||||||
|
|
||||||
let container = self
|
let container = self
|
||||||
.focused_container_mut()
|
.focused_container_mut()
|
||||||
.context("there is no container")?;
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
|
|
||||||
let window = container
|
let window = container
|
||||||
.remove_focused_window()
|
.remove_focused_window()
|
||||||
.context("there is no window")?;
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
if container.windows().is_empty() {
|
if container.windows().is_empty() {
|
||||||
self.containers_mut().remove(focused_container_idx);
|
self.containers_mut().remove(focused_container_idx);
|
||||||
@@ -458,7 +639,7 @@ impl Workspace {
|
|||||||
let focused_idx = self.focused_container_idx();
|
let focused_idx = self.focused_container_idx();
|
||||||
let window = self
|
let window = self
|
||||||
.remove_focused_floating_window()
|
.remove_focused_floating_window()
|
||||||
.context("there is no floating window")?;
|
.ok_or_else(|| anyhow!("there is no floating window"))?;
|
||||||
|
|
||||||
let mut container = Container::default();
|
let mut container = Container::default();
|
||||||
container.add_window(window);
|
container.add_window(window);
|
||||||
@@ -494,22 +675,44 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_floating_window(&mut self) -> Result<()> {
|
pub fn new_floating_window(&mut self) -> Result<()> {
|
||||||
let focused_idx = self.focused_container_idx();
|
let window = if let Some(maximized_window) = self.maximized_window() {
|
||||||
|
let window = *maximized_window;
|
||||||
|
self.set_maximized_window(None);
|
||||||
|
self.set_maximized_window_restore_idx(None);
|
||||||
|
window
|
||||||
|
} else if let Some(monocle_container) = self.monocle_container_mut() {
|
||||||
|
let window = monocle_container
|
||||||
|
.remove_focused_window()
|
||||||
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
let container = self
|
if monocle_container.windows().is_empty() {
|
||||||
.focused_container_mut()
|
self.set_monocle_container(None);
|
||||||
.context("there is no container")?;
|
self.set_monocle_container_restore_idx(None);
|
||||||
|
} else {
|
||||||
|
monocle_container.load_focused_window();
|
||||||
|
}
|
||||||
|
|
||||||
let window = container
|
window
|
||||||
.remove_focused_window()
|
|
||||||
.context("there is no window")?;
|
|
||||||
|
|
||||||
if container.windows().is_empty() {
|
|
||||||
self.containers_mut().remove(focused_idx);
|
|
||||||
self.resize_dimensions_mut().remove(focused_idx);
|
|
||||||
} else {
|
} else {
|
||||||
container.load_focused_window();
|
let focused_idx = self.focused_container_idx();
|
||||||
}
|
|
||||||
|
let container = self
|
||||||
|
.focused_container_mut()
|
||||||
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
|
|
||||||
|
let window = container
|
||||||
|
.remove_focused_window()
|
||||||
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
|
if container.windows().is_empty() {
|
||||||
|
self.containers_mut().remove(focused_idx);
|
||||||
|
self.resize_dimensions_mut().remove(focused_idx);
|
||||||
|
} else {
|
||||||
|
container.load_focused_window();
|
||||||
|
}
|
||||||
|
|
||||||
|
window
|
||||||
|
};
|
||||||
|
|
||||||
self.floating_windows_mut().push(window);
|
self.floating_windows_mut().push(window);
|
||||||
|
|
||||||
@@ -547,7 +750,7 @@ impl Workspace {
|
|||||||
let container = self
|
let container = self
|
||||||
.containers_mut()
|
.containers_mut()
|
||||||
.remove(focused_idx)
|
.remove(focused_idx)
|
||||||
.context("there is not container")?;
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
|
|
||||||
// We don't remove any resize adjustments for a monocle, because when this container is
|
// We don't remove any resize adjustments for a monocle, because when this container is
|
||||||
// inevitably reintegrated, it would be weird if it doesn't go back to the dimensions
|
// inevitably reintegrated, it would be weird if it doesn't go back to the dimensions
|
||||||
@@ -559,7 +762,7 @@ impl Workspace {
|
|||||||
|
|
||||||
self.monocle_container_mut()
|
self.monocle_container_mut()
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.context("there is no monocle container")?
|
.ok_or_else(|| anyhow!("there is no monocle container"))?
|
||||||
.load_focused_window();
|
.load_focused_window();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -568,12 +771,12 @@ impl Workspace {
|
|||||||
pub fn reintegrate_monocle_container(&mut self) -> Result<()> {
|
pub fn reintegrate_monocle_container(&mut self) -> Result<()> {
|
||||||
let restore_idx = self
|
let restore_idx = self
|
||||||
.monocle_container_restore_idx()
|
.monocle_container_restore_idx()
|
||||||
.context("there is no monocle restore index")?;
|
.ok_or_else(|| anyhow!("there is no monocle restore index"))?;
|
||||||
|
|
||||||
let container = self
|
let container = self
|
||||||
.monocle_container_mut()
|
.monocle_container_mut()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.context("there is no monocle container")?;
|
.ok_or_else(|| anyhow!("there is no monocle container"))?;
|
||||||
|
|
||||||
let container = container.clone();
|
let container = container.clone();
|
||||||
if restore_idx > self.containers().len() - 1 {
|
if restore_idx > self.containers().len() - 1 {
|
||||||
@@ -584,7 +787,7 @@ impl Workspace {
|
|||||||
self.containers_mut().insert(restore_idx, container);
|
self.containers_mut().insert(restore_idx, container);
|
||||||
self.focus_container(restore_idx);
|
self.focus_container(restore_idx);
|
||||||
self.focused_container_mut()
|
self.focused_container_mut()
|
||||||
.context("there is no container")?
|
.ok_or_else(|| anyhow!("there is no container"))?
|
||||||
.load_focused_window();
|
.load_focused_window();
|
||||||
|
|
||||||
self.set_monocle_container(None);
|
self.set_monocle_container(None);
|
||||||
@@ -595,14 +798,61 @@ impl Workspace {
|
|||||||
|
|
||||||
pub fn new_maximized_window(&mut self) -> Result<()> {
|
pub fn new_maximized_window(&mut self) -> Result<()> {
|
||||||
let focused_idx = self.focused_container_idx();
|
let focused_idx = self.focused_container_idx();
|
||||||
|
let foreground_hwnd = WindowsApi::foreground_window()?;
|
||||||
|
let mut floating_window = None;
|
||||||
|
|
||||||
|
if !self.floating_windows().is_empty() {
|
||||||
|
let mut focused_floating_window_idx = None;
|
||||||
|
for (i, w) in self.floating_windows().iter().enumerate() {
|
||||||
|
if w.hwnd == foreground_hwnd {
|
||||||
|
focused_floating_window_idx = Option::from(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(idx) = focused_floating_window_idx {
|
||||||
|
floating_window = Option::from(self.floating_windows_mut().remove(idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(floating_window) = floating_window {
|
||||||
|
self.set_maximized_window(Option::from(floating_window));
|
||||||
|
self.set_maximized_window_restore_idx(Option::from(focused_idx));
|
||||||
|
if let Some(window) = self.maximized_window() {
|
||||||
|
window.maximize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let monocle_restore_idx = self.monocle_container_restore_idx();
|
||||||
|
if let Some(monocle_container) = self.monocle_container_mut() {
|
||||||
|
let window = monocle_container
|
||||||
|
.remove_focused_window()
|
||||||
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
|
if monocle_container.windows().is_empty() {
|
||||||
|
self.set_monocle_container(None);
|
||||||
|
self.set_monocle_container_restore_idx(None);
|
||||||
|
} else {
|
||||||
|
monocle_container.load_focused_window();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_maximized_window(Option::from(window));
|
||||||
|
self.set_maximized_window_restore_idx(monocle_restore_idx);
|
||||||
|
if let Some(window) = self.maximized_window() {
|
||||||
|
window.maximize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let container = self
|
let container = self
|
||||||
.focused_container_mut()
|
.focused_container_mut()
|
||||||
.context("there is no container")?;
|
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||||
|
|
||||||
let window = container
|
let window = container
|
||||||
.remove_focused_window()
|
.remove_focused_window()
|
||||||
.context("there is no window")?;
|
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||||
|
|
||||||
if container.windows().is_empty() {
|
if container.windows().is_empty() {
|
||||||
self.containers_mut().remove(focused_idx);
|
self.containers_mut().remove(focused_idx);
|
||||||
@@ -626,12 +876,12 @@ impl Workspace {
|
|||||||
pub fn reintegrate_maximized_window(&mut self) -> Result<()> {
|
pub fn reintegrate_maximized_window(&mut self) -> Result<()> {
|
||||||
let restore_idx = self
|
let restore_idx = self
|
||||||
.maximized_window_restore_idx()
|
.maximized_window_restore_idx()
|
||||||
.context("there is no monocle restore index")?;
|
.ok_or_else(|| anyhow!("there is no monocle restore index"))?;
|
||||||
|
|
||||||
let window = self
|
let window = self
|
||||||
.maximized_window()
|
.maximized_window()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.context("there is no monocle container")?;
|
.ok_or_else(|| anyhow!("there is no monocle container"))?;
|
||||||
|
|
||||||
let window = *window;
|
let window = *window;
|
||||||
if !self.containers().is_empty() && restore_idx > self.containers().len() - 1 {
|
if !self.containers().is_empty() && restore_idx > self.containers().len() - 1 {
|
||||||
@@ -646,7 +896,7 @@ impl Workspace {
|
|||||||
self.focus_container(restore_idx);
|
self.focus_container(restore_idx);
|
||||||
|
|
||||||
self.focused_container_mut()
|
self.focused_container_mut()
|
||||||
.context("there is no container")?
|
.ok_or_else(|| anyhow!("there is no container"))?
|
||||||
.load_focused_window();
|
.load_focused_window();
|
||||||
|
|
||||||
self.set_maximized_window(None);
|
self.set_maximized_window(None);
|
||||||
|
|||||||
341
komorebic.lib.sample.ahk
Normal file
341
komorebic.lib.sample.ahk
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
; Generated by komorebic.exe
|
||||||
|
|
||||||
|
Start(ffm, await_configuration) {
|
||||||
|
Run, komorebic.exe start %ffm% --await_configuration %await_configuration%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Stop() {
|
||||||
|
Run, komorebic.exe stop, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
State() {
|
||||||
|
Run, komorebic.exe state, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Query(state_query) {
|
||||||
|
Run, komorebic.exe query %state_query%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Subscribe(named_pipe) {
|
||||||
|
Run, komorebic.exe subscribe %named_pipe%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Unsubscribe(named_pipe) {
|
||||||
|
Run, komorebic.exe unsubscribe %named_pipe%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Log() {
|
||||||
|
Run, komorebic.exe log, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
QuickSaveResize() {
|
||||||
|
Run, komorebic.exe quick-save-resize, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
QuickLoadResize() {
|
||||||
|
Run, komorebic.exe quick-load-resize, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveResize(path) {
|
||||||
|
Run, komorebic.exe save-resize %path%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadResize(path) {
|
||||||
|
Run, komorebic.exe load-resize %path%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Focus(operation_direction) {
|
||||||
|
Run, komorebic.exe focus %operation_direction%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Move(operation_direction) {
|
||||||
|
Run, komorebic.exe move %operation_direction%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
CycleFocus(cycle_direction) {
|
||||||
|
Run, komorebic.exe cycle-focus %cycle_direction%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
CycleMove(cycle_direction) {
|
||||||
|
Run, komorebic.exe cycle-move %cycle_direction%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Stack(operation_direction) {
|
||||||
|
Run, komorebic.exe stack %operation_direction%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Resize(edge, sizing) {
|
||||||
|
Run, komorebic.exe resize %edge% %sizing%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ResizeAxis(axis, sizing) {
|
||||||
|
Run, komorebic.exe resize-axis %axis% %sizing%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Unstack() {
|
||||||
|
Run, komorebic.exe unstack, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
CycleStack(cycle_direction) {
|
||||||
|
Run, komorebic.exe cycle-stack %cycle_direction%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveToMonitor(target) {
|
||||||
|
Run, komorebic.exe move-to-monitor %target%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveToWorkspace(target) {
|
||||||
|
Run, komorebic.exe move-to-workspace %target%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
SendToMonitor(target) {
|
||||||
|
Run, komorebic.exe send-to-monitor %target%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
SendToWorkspace(target) {
|
||||||
|
Run, komorebic.exe send-to-workspace %target%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
SendToMonitorWorkspace(target_monitor, target_workspace) {
|
||||||
|
Run, komorebic.exe send-to-monitor-workspace %target_monitor% %target_workspace%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusMonitor(target) {
|
||||||
|
Run, komorebic.exe focus-monitor %target%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusWorkspace(target) {
|
||||||
|
Run, komorebic.exe focus-workspace %target%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusMonitorWorkspace(target_monitor, target_workspace) {
|
||||||
|
Run, komorebic.exe focus-monitor-workspace %target_monitor% %target_workspace%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
CycleMonitor(cycle_direction) {
|
||||||
|
Run, komorebic.exe cycle-monitor %cycle_direction%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
CycleWorkspace(cycle_direction) {
|
||||||
|
Run, komorebic.exe cycle-workspace %cycle_direction%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveWorkspaceToMonitor(target) {
|
||||||
|
Run, komorebic.exe move-workspace-to-monitor %target%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
NewWorkspace() {
|
||||||
|
Run, komorebic.exe new-workspace, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ResizeDelta(pixels) {
|
||||||
|
Run, komorebic.exe resize-delta %pixels%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
InvisibleBorders(left, top, right, bottom) {
|
||||||
|
Run, komorebic.exe invisible-borders %left% %top% %right% %bottom%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkAreaOffset(left, top, right, bottom) {
|
||||||
|
Run, komorebic.exe work-area-offset %left% %top% %right% %bottom%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
AdjustContainerPadding(sizing, adjustment) {
|
||||||
|
Run, komorebic.exe adjust-container-padding %sizing% %adjustment%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
AdjustWorkspacePadding(sizing, adjustment) {
|
||||||
|
Run, komorebic.exe adjust-workspace-padding %sizing% %adjustment%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangeLayout(default_layout) {
|
||||||
|
Run, komorebic.exe change-layout %default_layout%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadCustomLayout(path) {
|
||||||
|
Run, komorebic.exe load-custom-layout %path%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
FlipLayout(axis) {
|
||||||
|
Run, komorebic.exe flip-layout %axis%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Promote() {
|
||||||
|
Run, komorebic.exe promote, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Retile() {
|
||||||
|
Run, komorebic.exe retile, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureWorkspaces(monitor, workspace_count) {
|
||||||
|
Run, komorebic.exe ensure-workspaces %monitor% %workspace_count%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ContainerPadding(monitor, workspace, size) {
|
||||||
|
Run, komorebic.exe container-padding %monitor% %workspace% %size%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspacePadding(monitor, workspace, size) {
|
||||||
|
Run, komorebic.exe workspace-padding %monitor% %workspace% %size%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceLayout(monitor, workspace, value) {
|
||||||
|
Run, komorebic.exe workspace-layout %monitor% %workspace% %value%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceCustomLayout(monitor, workspace, path) {
|
||||||
|
Run, komorebic.exe workspace-custom-layout %monitor% %workspace% %path%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceLayoutRule(monitor, workspace, at_container_count, layout) {
|
||||||
|
Run, komorebic.exe workspace-layout-rule %monitor% %workspace% %at_container_count% %layout%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceCustomLayoutRule(monitor, workspace, at_container_count, path) {
|
||||||
|
Run, komorebic.exe workspace-custom-layout-rule %monitor% %workspace% %at_container_count% %path%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearWorkspaceLayoutRules(monitor, workspace) {
|
||||||
|
Run, komorebic.exe clear-workspace-layout-rules %monitor% %workspace%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceTiling(monitor, workspace, value) {
|
||||||
|
Run, komorebic.exe workspace-tiling %monitor% %workspace% %value%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceName(monitor, workspace, value) {
|
||||||
|
Run, komorebic.exe workspace-name %monitor% %workspace% %value%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleWindowContainerBehaviour() {
|
||||||
|
Run, komorebic.exe toggle-window-container-behaviour, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
TogglePause() {
|
||||||
|
Run, komorebic.exe toggle-pause, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleTiling() {
|
||||||
|
Run, komorebic.exe toggle-tiling, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleFloat() {
|
||||||
|
Run, komorebic.exe toggle-float, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleMonocle() {
|
||||||
|
Run, komorebic.exe toggle-monocle, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleMaximize() {
|
||||||
|
Run, komorebic.exe toggle-maximize, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
RestoreWindows() {
|
||||||
|
Run, komorebic.exe restore-windows, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Manage() {
|
||||||
|
Run, komorebic.exe manage, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
Unmanage() {
|
||||||
|
Run, komorebic.exe unmanage, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ReloadConfiguration() {
|
||||||
|
Run, komorebic.exe reload-configuration, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WatchConfiguration(boolean_state) {
|
||||||
|
Run, komorebic.exe watch-configuration %boolean_state%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
CompleteConfiguration() {
|
||||||
|
Run, komorebic.exe complete-configuration, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowHidingBehaviour(hiding_behaviour) {
|
||||||
|
Run, komorebic.exe window-hiding-behaviour %hiding_behaviour%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
CrossMonitorMoveBehaviour(move_behaviour) {
|
||||||
|
Run, komorebic.exe cross-monitor-move-behaviour %move_behaviour%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleCrossMonitorMoveBehaviour() {
|
||||||
|
Run, komorebic.exe toggle-cross-monitor-move-behaviour, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
UnmanagedWindowOperationBehaviour(operation_behaviour) {
|
||||||
|
Run, komorebic.exe unmanaged-window-operation-behaviour %operation_behaviour%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatRule(identifier, id) {
|
||||||
|
Run, komorebic.exe float-rule %identifier% "%id%", , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ManageRule(identifier, id) {
|
||||||
|
Run, komorebic.exe manage-rule %identifier% "%id%", , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceRule(identifier, id, monitor, workspace) {
|
||||||
|
Run, komorebic.exe workspace-rule %identifier% "%id%" %monitor% %workspace%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentifyObjectNameChangeApplication(identifier, id) {
|
||||||
|
Run, komorebic.exe identify-object-name-change-application %identifier% "%id%", , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentifyTrayApplication(identifier, id) {
|
||||||
|
Run, komorebic.exe identify-tray-application %identifier% "%id%", , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentifyLayeredApplication(identifier, id) {
|
||||||
|
Run, komorebic.exe identify-layered-application %identifier% "%id%", , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentifyBorderOverflowApplication(identifier, id) {
|
||||||
|
Run, komorebic.exe identify-border-overflow-application %identifier% "%id%", , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveWindowBorder(boolean_state) {
|
||||||
|
Run, komorebic.exe active-window-border %boolean_state%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveWindowBorderColour(r, g, b) {
|
||||||
|
Run, komorebic.exe active-window-border-colour %r% %g% %b%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusFollowsMouse(boolean_state, implementation) {
|
||||||
|
Run, komorebic.exe focus-follows-mouse %boolean_state% --implementation %implementation%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleFocusFollowsMouse(implementation) {
|
||||||
|
Run, komorebic.exe toggle-focus-follows-mouse --implementation %implementation%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseFollowsFocus(boolean_state) {
|
||||||
|
Run, komorebic.exe mouse-follows-focus %boolean_state%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleMouseFollowsFocus() {
|
||||||
|
Run, komorebic.exe toggle-mouse-follows-focus, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
AhkLibrary() {
|
||||||
|
Run, komorebic.exe ahk-library, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
AhkAppSpecificConfiguration(path, override_path) {
|
||||||
|
Run, komorebic.exe ahk-app-specific-configuration %path% %override_path%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
FormatAppSpecificConfiguration(path) {
|
||||||
|
Run, komorebic.exe format-app-specific-configuration %path%, , Hide
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationSchema() {
|
||||||
|
Run, komorebic.exe notification-schema, , Hide
|
||||||
|
}
|
||||||
@@ -1,25 +1,35 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "komorebic"
|
name = "komorebic"
|
||||||
version = "0.1.1"
|
version = "0.1.11"
|
||||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||||
categories = ["cli", "tiling-window-manager", "windows"]
|
categories = ["cli", "tiling-window-manager", "windows"]
|
||||||
repository = "https://github.com/LGUG2Z/komorebi"
|
repository = "https://github.com/LGUG2Z/komorebi"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bindings = { package = "bindings", path = "../bindings" }
|
derive-ahk = { path = "../derive-ahk" }
|
||||||
komorebi-core = { path = "../komorebi-core" }
|
komorebi-core = { path = "../komorebi-core" }
|
||||||
|
|
||||||
clap = "3.0.0-beta.4"
|
clap = { version = "3", features = ["derive", "wrap_help"] }
|
||||||
color-eyre = "0.5"
|
color-eyre = "0.6"
|
||||||
dirs = "3"
|
dirs = "4"
|
||||||
fs-tail = "0.1"
|
fs-tail = "0.1"
|
||||||
|
heck = "0.4"
|
||||||
|
lazy_static = "1"
|
||||||
paste = "1"
|
paste = "1"
|
||||||
powershell_script = "0.2"
|
powershell_script = "1.0"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
serde_yaml = "0.9"
|
||||||
uds_windows = "1"
|
uds_windows = "1"
|
||||||
|
|
||||||
|
[dependencies.windows]
|
||||||
|
version = "0.39"
|
||||||
|
features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_UI_WindowsAndMessaging"
|
||||||
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
imports_granularity = "Item"
|
||||||
Reference in New Issue
Block a user