mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-13 22:13:02 +01:00
Compare commits
164 Commits
feature/po
...
feature/az
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7435089010 | ||
|
|
faa7786979 | ||
|
|
3c8a6cb7bd | ||
|
|
50a279239a | ||
|
|
2c8f25ef82 | ||
|
|
bdc1cad597 | ||
|
|
e2f2d6b919 | ||
|
|
aebe8792ac | ||
|
|
9fb6f8ebcd | ||
|
|
6eb6129618 | ||
|
|
5b997b6ea6 | ||
|
|
9414466646 | ||
|
|
9e87baa8b8 | ||
|
|
2a67c9c786 | ||
|
|
60bc96e9a5 | ||
|
|
5e9d573f0b | ||
|
|
128db85054 | ||
|
|
cc7dbde049 | ||
|
|
a5735c4186 | ||
|
|
48cb3db2fe | ||
|
|
24bff7e527 | ||
|
|
b985659e30 | ||
|
|
1d0ac9b555 | ||
|
|
7b1ece9680 | ||
|
|
00fc5382f6 | ||
|
|
9e37baa88a | ||
|
|
9a65a4ae92 | ||
|
|
a511cbd263 | ||
|
|
83cc7bf7c0 | ||
|
|
8bf4ab9f15 | ||
|
|
31864b1570 | ||
|
|
c022438a37 | ||
|
|
3d518f73ca | ||
|
|
d2d6484e38 | ||
|
|
67a3c3546f | ||
|
|
888b674646 | ||
|
|
5abab46290 | ||
|
|
ad8375eebe | ||
|
|
a11da2167c | ||
|
|
6b9a0843fd | ||
|
|
41732e2f5f | ||
|
|
9a58c1ee42 | ||
|
|
edc87d9940 | ||
|
|
133311bbe2 | ||
|
|
280aebf15d | ||
|
|
a488890a04 | ||
|
|
9a0ee8e8dd | ||
|
|
f23510055a | ||
|
|
a5fb5527c6 | ||
|
|
3f6e19b8b4 | ||
|
|
aa24c41967 | ||
|
|
1320b7440e | ||
|
|
cad2eb9a63 | ||
|
|
b7a987be8f | ||
|
|
e8f6a66bed | ||
|
|
fd97c7230d | ||
|
|
cc60f55cec | ||
|
|
270374497c | ||
|
|
6c90001c00 | ||
|
|
3232d9242a | ||
|
|
e57b08d073 | ||
|
|
cfb0c7f2ce | ||
|
|
5cff90a62b | ||
|
|
da7a9394d8 | ||
|
|
88684f991f | ||
|
|
03fdbea5cd | ||
|
|
340c137342 | ||
|
|
e46a0757e3 | ||
|
|
3556f38469 | ||
|
|
62770033f2 | ||
|
|
e294dbbe93 | ||
|
|
47f0ab1ef3 | ||
|
|
c4d62fc4f6 | ||
|
|
69680b4238 | ||
|
|
0dc17e9cb3 | ||
|
|
0f44efaa82 | ||
|
|
05af7ce16a | ||
|
|
92447723d2 | ||
|
|
2a45f981e6 | ||
|
|
a29ab4cfb3 | ||
|
|
27cd1736aa | ||
|
|
d2470b1f08 | ||
|
|
835472d739 | ||
|
|
bceb28de37 | ||
|
|
fff7b5c147 | ||
|
|
1e63947ae3 | ||
|
|
d5f4f916be | ||
|
|
77fc3973b6 | ||
|
|
29b1794409 | ||
|
|
1420334c94 | ||
|
|
82aa2edf8f | ||
|
|
228cb26b64 | ||
|
|
3d53c602a7 | ||
|
|
81f741bbbd | ||
|
|
0330dfe250 | ||
|
|
70ef90b304 | ||
|
|
d102c00ffe | ||
|
|
87b1ab9c53 | ||
|
|
a4dd5fc741 | ||
|
|
226ee73aa4 | ||
|
|
e14235c3a9 | ||
|
|
855bb49804 | ||
|
|
07b2da69a1 | ||
|
|
6a1ed3bcaa | ||
|
|
1b30561989 | ||
|
|
c47cf4718b | ||
|
|
1accbf65ca | ||
|
|
7ee3c928d8 | ||
|
|
6d1903099a | ||
|
|
d5c6f090cc | ||
|
|
598f9ec0aa | ||
|
|
11acff5236 | ||
|
|
4802b55452 | ||
|
|
482a7b1d7f | ||
|
|
d00ee82a9d | ||
|
|
9f01d8fa0f | ||
|
|
627088c9b9 | ||
|
|
22cf7b5017 | ||
|
|
3e984d886c | ||
|
|
185cb4d4a8 | ||
|
|
62900c59cb | ||
|
|
a2e9a46582 | ||
|
|
871a53821c | ||
|
|
bcd1c50d82 | ||
|
|
0c41d9ded2 | ||
|
|
4af62fe97b | ||
|
|
7cab062124 | ||
|
|
611fa34567 | ||
|
|
95990d682b | ||
|
|
e363a494c3 | ||
|
|
383533e2d9 | ||
|
|
0b04e3ef93 | ||
|
|
3370e6acc5 | ||
|
|
4ffffc5eec | ||
|
|
2b5f737d14 | ||
|
|
8a455c8ab7 | ||
|
|
3d9871c576 | ||
|
|
cafb8e9a48 | ||
|
|
740cb3c877 | ||
|
|
b8b3b3d615 | ||
|
|
6fce630be5 | ||
|
|
dfd0d604aa | ||
|
|
aff1081ccd | ||
|
|
793e81d43d | ||
|
|
eac4c8e9b1 | ||
|
|
6b42587af4 | ||
|
|
46152621c0 | ||
|
|
b78693118b | ||
|
|
7caa839a00 | ||
|
|
1671f31e3e | ||
|
|
6fe46610fc | ||
|
|
4ba3125dde | ||
|
|
efa562de5c | ||
|
|
b476bee1d8 | ||
|
|
86b07f28dd | ||
|
|
311e37c8a2 | ||
|
|
15c3b32608 | ||
|
|
28b46c54da | ||
|
|
732aca77b5 | ||
|
|
f56fc36557 | ||
|
|
5334e1944e | ||
|
|
d8d087e621 | ||
|
|
b61146ead4 | ||
|
|
16cb811aa9 |
30
.github/workflows/windows.yaml
vendored
30
.github/workflows/windows.yaml
vendored
@@ -14,14 +14,16 @@ on:
|
||||
tags:
|
||||
- v*
|
||||
schedule:
|
||||
- cron: "30 0 * * 1" # Every Monday at half past midnight UTC
|
||||
- cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: windows-latest
|
||||
permissions: write-all
|
||||
env:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -93,17 +95,39 @@ jobs:
|
||||
target/${{ matrix.target }}/release/komorebi.exe
|
||||
target/${{ matrix.target }}/release/komorebic.exe
|
||||
target/${{ matrix.target }}/release/komorebic-no-console.exe
|
||||
target/${{ matrix.target }}/release/komorebi-gui.exe
|
||||
target/${{ matrix.target }}/release/komorebi.pdb
|
||||
target/${{ matrix.target }}/release/komorebic.pdb
|
||||
target/${{ matrix.target }}/release/komorebic-no-console.pdb
|
||||
target/${{ matrix.target }}/release/komorebi_gui.pdb
|
||||
target/wix/komorebi-*.msi
|
||||
retention-days: 7
|
||||
- name: Check GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
env:
|
||||
GORELEASER_CURRENT_TAG: v0.1.28
|
||||
with:
|
||||
version: latest
|
||||
args: build --skip=validate --clean
|
||||
|
||||
- name: Prepare nightly artifacts
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
|
||||
run: |
|
||||
Compress-Archive .\target\${{ matrix.target }}\release\*.exe komorebi-nightly-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./target/wix/*.msi -Destination ./komorebi-nightly-x86_64.msi
|
||||
echo "$((Get-FileHash komorebi-nightly-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-nightly-x86_64-pc-windows-msvc.zip" >checksums.txt
|
||||
- name: Update nightly
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
|
||||
shell: bash
|
||||
run: |
|
||||
gh release delete nightly --yes || true
|
||||
git push origin :nightly || true
|
||||
gh release create nightly \
|
||||
--target $GITHUB_SHA \
|
||||
--prerelease \
|
||||
--title "komorebi nightly (${GITHUB_SHA})" \
|
||||
--notes "This nightly release of komorebi corresponds to [this commit](https://github.com/LGUG2Z/komorebi/commit/${GITHUB_SHA})." \
|
||||
komorebi-nightly-x86_64-pc-windows-msvc.zip \
|
||||
komorebi-nightly-x86_64.msi \
|
||||
checksums.txt
|
||||
# Release
|
||||
- name: Generate changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,5 +3,4 @@
|
||||
/target
|
||||
CHANGELOG.md
|
||||
dummy.go
|
||||
komorebi.ahk
|
||||
komorebic/applications.yaml
|
||||
komorebic/applications.yaml
|
||||
|
||||
@@ -10,8 +10,8 @@ before:
|
||||
builds:
|
||||
- id: komorebi
|
||||
main: dummy.go
|
||||
goos: ["windows"]
|
||||
goarch: ["amd64"]
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebi
|
||||
hooks:
|
||||
post:
|
||||
@@ -19,8 +19,8 @@ builds:
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi.exe" ".\dist\komorebi_windows_amd64_v1\komorebi.exe"
|
||||
- id: komorebic
|
||||
main: dummy.go
|
||||
goos: ["windows"]
|
||||
goarch: ["amd64"]
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebic
|
||||
hooks:
|
||||
post:
|
||||
@@ -28,19 +28,28 @@ builds:
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64_v1\komorebic.exe"
|
||||
- id: komorebic-no-console
|
||||
main: dummy.go
|
||||
goos: ["windows"]
|
||||
goarch: ["amd64"]
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebic-no-console
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic-no-console.exe" ".\dist\komorebic-no-console_windows_amd64_v1\komorebic-no-console.exe"
|
||||
- id: komorebi-gui
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebi-gui
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-gui.exe" ".\dist\komorebi-gui_windows_amd64_v1\komorebi-gui.exe"
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE
|
||||
- LICENSE.md
|
||||
- CHANGELOG.md
|
||||
|
||||
checksum:
|
||||
|
||||
45
CONTRIBUTING.md
Normal file
45
CONTRIBUTING.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Contributing to the Project
|
||||
|
||||
The project is a collection of contributions from both the project leaders and
|
||||
community members. There are many ways to contribute, this can include content
|
||||
in the project repositories, as well as contributing in public and private
|
||||
conversation, assisting users, writing blog posts, and many other ways.
|
||||
|
||||
## How contributions are made
|
||||
|
||||
Contributions to the project primarily happen in the project source
|
||||
repositories, but may also occur in other places, such as discussion forums and
|
||||
public and private discourse.
|
||||
|
||||
## Contributing content to the Project
|
||||
|
||||
In order for the project leaders to manage sustained progress toward the
|
||||
project goals and maintain project velocity, focus and quality, the project may
|
||||
adjust the license terms over time.
|
||||
|
||||
Content contributed to the project must therefore be provided under
|
||||
sufficiently liberal terms to allow these operations to proceed unimpeded. As
|
||||
such contributions are accepted with the following understanding:
|
||||
|
||||
* Contributed content is licensed under the terms of the 0-BSD license
|
||||
* Contributors accept the terms of the project license at the time of
|
||||
contribution
|
||||
|
||||
By making a contribution, you accept both the current project license terms,
|
||||
and that all contributions that you have made are provided under the terms of
|
||||
the 0-BSD license.
|
||||
|
||||
## Zero-Clause BSD
|
||||
|
||||
```
|
||||
Permission to use, copy, modify, and/or distribute this software for
|
||||
any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
|
||||
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
||||
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
```
|
||||
3577
Cargo.lock
generated
3577
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@@ -2,21 +2,26 @@
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"derive-ahk",
|
||||
"komorebi",
|
||||
"komorebi-client",
|
||||
"komorebi-core",
|
||||
"komorebi-gui",
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
windows-interface = { version = "0.53" }
|
||||
windows-implement = { version = "0.53" }
|
||||
dunce = "1"
|
||||
dirs = "5"
|
||||
color-eyre = "0.6"
|
||||
serde_json = { package = "serde_json_lenient", version = "0.1" }
|
||||
dirs = "5"
|
||||
dunce = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
||||
sysinfo = "0.30"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "32a45cebf132c3d651ee22c0c40033a6b7edc945" }
|
||||
windows-implement = { version = "0.53" }
|
||||
windows-interface = { version = "0.53" }
|
||||
shadow-rs = "0.29"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.54"
|
||||
@@ -36,5 +41,6 @@ features = [
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_SystemServices"
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming"
|
||||
]
|
||||
|
||||
21
LICENSE
21
LICENSE
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Jade Iqbal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
105
LICENSE.md
Normal file
105
LICENSE.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# PolyForm Strict License 1.0.0
|
||||
|
||||
<https://polyformproject.org/licenses/strict/1.0.0>
|
||||
|
||||
## Acceptance
|
||||
|
||||
In order to get any license under these terms, you must agree
|
||||
to them as both strict obligations and conditions to all
|
||||
your licenses.
|
||||
|
||||
## Copyright License
|
||||
|
||||
The licensor grants you a copyright license for the software
|
||||
to do everything you might do with the software that would
|
||||
otherwise infringe the licensor's copyright in it for any
|
||||
permitted purpose, other than distributing the software or
|
||||
making changes or new works based on the software.
|
||||
|
||||
## Patent License
|
||||
|
||||
The licensor grants you a patent license for the software that
|
||||
covers patent claims the licensor can license, or becomes able
|
||||
to license, that you would infringe by using the software.
|
||||
|
||||
## Noncommercial Purposes
|
||||
|
||||
Any noncommercial purpose is a permitted purpose.
|
||||
|
||||
## Personal Uses
|
||||
|
||||
Personal use for research, experiment, and testing for
|
||||
the benefit of public knowledge, personal study, private
|
||||
entertainment, hobby projects, amateur pursuits, or religious
|
||||
observance, without any anticipated commercial application,
|
||||
is use for a permitted purpose.
|
||||
|
||||
## Noncommercial Organizations
|
||||
|
||||
Use by any charitable organization, educational institution,
|
||||
public research organization, public safety or health
|
||||
organization, environmental protection organization,
|
||||
or government institution is use for a permitted purpose
|
||||
regardless of the source of funding or obligations resulting
|
||||
from the funding.
|
||||
|
||||
## Fair Use
|
||||
|
||||
You may have "fair use" rights for the software under the
|
||||
law. These terms do not limit them.
|
||||
|
||||
## No Other Rights
|
||||
|
||||
These terms do not allow you to sublicense or transfer any of
|
||||
your licenses to anyone else, or prevent the licensor from
|
||||
granting licenses to anyone else. These terms do not imply
|
||||
any other licenses.
|
||||
|
||||
## Patent Defense
|
||||
|
||||
If you make any written claim that the software infringes or
|
||||
contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If
|
||||
your company makes such a claim, your patent license ends
|
||||
immediately for work on behalf of your company.
|
||||
|
||||
## Violations
|
||||
|
||||
The first time you are notified in writing that you have
|
||||
violated any of these terms, or done anything with the software
|
||||
not covered by your licenses, your licenses can nonetheless
|
||||
continue if you come into full compliance with these terms,
|
||||
and take practical steps to correct past violations, within
|
||||
32 days of receiving notice. Otherwise, all your licenses
|
||||
end immediately.
|
||||
|
||||
## No Liability
|
||||
|
||||
***As far as the law allows, the software comes as is, without
|
||||
any warranty or condition, and the licensor will not be liable
|
||||
to you for any damages arising out of these terms or the use
|
||||
or nature of the software, under any kind of legal claim.***
|
||||
|
||||
## Definitions
|
||||
|
||||
The **licensor** is the individual or entity offering these
|
||||
terms, and the **software** is the software the licensor makes
|
||||
available under these terms.
|
||||
|
||||
**You** refers to the individual or entity agreeing to these
|
||||
terms.
|
||||
|
||||
**Your company** is any legal entity, sole proprietorship,
|
||||
or other kind of organization that you work for, plus all
|
||||
organizations that have control over, are under the control of,
|
||||
or are under common control with that organization. **Control**
|
||||
means ownership of substantially all the assets of an entity,
|
||||
or the power to direct its management and policies by vote,
|
||||
contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
**Your licenses** are all the licenses granted to you for the
|
||||
software under these terms.
|
||||
|
||||
**Use** means anything you do with the software requiring one
|
||||
of your licenses.
|
||||
|
||||
38
README.md
38
README.md
@@ -60,7 +60,7 @@ channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
|
||||
_komorebi_ development videos. If you would like to be notified of upcoming
|
||||
videos please subscribe and turn on notifications.
|
||||
|
||||
_komorebi_ is a free and open-source project, and one that encourages you to
|
||||
_komorebi_ is a free and source-available project, and one that encourages you to
|
||||
make charitable donations if you find the software to be useful and have the
|
||||
financial means.
|
||||
|
||||
@@ -84,8 +84,28 @@ using `scoop`, `winget` or building from source.
|
||||
|
||||
[](https://www.youtube.com/watch?v=H9-_c1egQ4g)
|
||||
|
||||
# Comparison With Fancy Zones
|
||||
|
||||
Community member [Olge](https://www.youtube.com/@polle5555) has created an
|
||||
excellent video which compares the default window management features of
|
||||
Windows 11, Fancy Zones and komorebi.
|
||||
|
||||
If you are not familiar with tiling window managers or if you are looking at
|
||||
komorebi and wondering "how is this different from Fancy Zones? 🤔", this short
|
||||
video will answer the majority of your questions.
|
||||
|
||||
[](https://www.youtube.com/watch?v=0LCbS_gm0RA)
|
||||
|
||||
|
||||
# Demonstrations
|
||||
|
||||
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28-dev.0` running on Windows 11 with window borders,
|
||||
unfocused window transparency and animations enabled, using a custom status bar integrated using
|
||||
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
|
||||
|
||||
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
|
||||
|
||||
|
||||
[@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
|
||||
@@ -169,6 +189,20 @@ ability for users to specify colours in `komorebi.json` in Hex format alongside
|
||||
There is also a process in place for graceful, non-breaking, deprecation of configuration options that are no longer
|
||||
required.
|
||||
|
||||
## License
|
||||
|
||||
`komorebi` is licensed under the [PolyForm Strict 1.0.0
|
||||
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
|
||||
this means that you are free to do whatever you want with `komorebi` other than
|
||||
redistribution, or distribution of new works (ie. hard-forks) based on the
|
||||
software.
|
||||
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended
|
||||
either for personal use or for integration back upstream via pull requests.
|
||||
|
||||
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
|
||||
code contributions to `komorebi` are licensed.
|
||||
|
||||
# Development
|
||||
|
||||
If you use IntelliJ, you should enable the following settings to ensure that code generated by macros is recognised by
|
||||
@@ -324,7 +358,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.24"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "derive-ahk"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
syn = "1.0"
|
||||
quote = "1.0"
|
||||
@@ -1,225 +0,0 @@
|
||||
#![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::format;
|
||||
use ::std::iter::Extend;
|
||||
use ::std::iter::Iterator;
|
||||
use ::std::matches;
|
||||
use ::std::option::Option::Some;
|
||||
use ::std::string::String;
|
||||
use ::std::string::ToString;
|
||||
use ::std::unreachable;
|
||||
use ::std::vec::Vec;
|
||||
|
||||
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("- - ", "--")
|
||||
.replace('_', "-");
|
||||
|
||||
let called_flag_arguments = quote! {#(%#flag_idents%) *}
|
||||
.to_string()
|
||||
.replace(" %", "%")
|
||||
.replace("% ", "%")
|
||||
.replace("%%", "% %");
|
||||
|
||||
let flags_split: Vec<_> = flags.split(' ').collect();
|
||||
let flag_args_split: Vec<_> = called_flag_arguments.split(' ').collect();
|
||||
let mut consolidated_flags: Vec<String> = Vec::new();
|
||||
|
||||
for (idx, flag) in flags_split.iter().enumerate() {
|
||||
consolidated_flags.push(format!("{} {}", flag, flag_args_split[idx]));
|
||||
}
|
||||
|
||||
let all_flags = consolidated_flags.join(" ");
|
||||
|
||||
quote! {
|
||||
impl AhkFunction for #name {
|
||||
fn generate_ahk_function() -> String {
|
||||
::std::format!(r#"
|
||||
{}({}) {{
|
||||
RunWait, komorebic.exe {} {} {}, , Hide
|
||||
}}"#,
|
||||
::std::stringify!(#name),
|
||||
#all_arguments,
|
||||
::std::stringify!(#name).to_kebab_case(),
|
||||
#called_arguments,
|
||||
#all_flags,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let arguments = quote! {#(#argument_idents), *}.to_string();
|
||||
|
||||
quote! {
|
||||
impl AhkFunction for #name {
|
||||
fn generate_ahk_function() -> String {
|
||||
::std::format!(r#"
|
||||
{}({}) {{
|
||||
RunWait, 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#"
|
||||
{}() {{
|
||||
RunWait, 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()
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
# active-window-border-colour
|
||||
|
||||
```
|
||||
Set the colour for the active window border
|
||||
|
||||
Usage: komorebic.exe active-window-border-colour [OPTIONS] <R> <G> <B>
|
||||
|
||||
Arguments:
|
||||
<R>
|
||||
Red
|
||||
|
||||
<G>
|
||||
Green
|
||||
|
||||
<B>
|
||||
Blue
|
||||
|
||||
Options:
|
||||
-w, --window-kind <WINDOW_KIND>
|
||||
[default: single]
|
||||
[possible values: single, stack, monocle]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
# active-window-border-offset
|
||||
|
||||
```
|
||||
Set the offset for the active window border
|
||||
|
||||
Usage: komorebic.exe active-window-border-offset <OFFSET>
|
||||
|
||||
Arguments:
|
||||
<OFFSET>
|
||||
Desired offset of the active window border
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
# active-window-border-width
|
||||
|
||||
```
|
||||
Set the width for the active window border
|
||||
|
||||
Usage: komorebic.exe active-window-border-width <WIDTH>
|
||||
|
||||
Arguments:
|
||||
<WIDTH>
|
||||
Desired width of the active window border
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,12 +0,0 @@
|
||||
# ahk-library
|
||||
|
||||
```
|
||||
Generate a library of AutoHotKey helper functions
|
||||
|
||||
Usage: komorebic.exe ahk-library
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
26
docs/cli/border-colour.md
Normal file
26
docs/cli/border-colour.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# border-colour
|
||||
|
||||
```
|
||||
Set the colour for a window border kind
|
||||
|
||||
Usage: komorebic.exe border-colour [OPTIONS] <R> <G> <B>
|
||||
|
||||
Arguments:
|
||||
<R>
|
||||
Red
|
||||
|
||||
<G>
|
||||
Green
|
||||
|
||||
<B>
|
||||
Blue
|
||||
|
||||
Options:
|
||||
-w, --window-kind <WINDOW_KIND>
|
||||
[default: single]
|
||||
[possible values: single, stack, monocle, unfocused]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
20
docs/cli/border-implementation.md
Normal file
20
docs/cli/border-implementation.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# border-implementation
|
||||
|
||||
```
|
||||
Set the border implementation
|
||||
|
||||
Usage: komorebic.exe border-implementation <STYLE>
|
||||
|
||||
Arguments:
|
||||
<STYLE>
|
||||
Desired border implementation
|
||||
|
||||
Possible values:
|
||||
- komorebi: Use the adjustable komorebi border implementation
|
||||
- windows: Use the thin Windows accent border implementation
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
```
|
||||
16
docs/cli/border-offset.md
Normal file
16
docs/cli/border-offset.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# border-offset
|
||||
|
||||
```
|
||||
Set the border offset
|
||||
|
||||
Usage: komorebic.exe border-offset <OFFSET>
|
||||
|
||||
Arguments:
|
||||
<OFFSET>
|
||||
Desired offset of the window border
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
21
docs/cli/border-style.md
Normal file
21
docs/cli/border-style.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# border-style
|
||||
|
||||
```
|
||||
Set the border style
|
||||
|
||||
Usage: komorebic.exe border-style <STYLE>
|
||||
|
||||
Arguments:
|
||||
<STYLE>
|
||||
Desired border style
|
||||
|
||||
Possible values:
|
||||
- system: Use the system border style
|
||||
- rounded: Use the Windows 11-style rounded borders
|
||||
- square: Use the Windows 10-style square borders
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
```
|
||||
16
docs/cli/border-width.md
Normal file
16
docs/cli/border-width.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# border-width
|
||||
|
||||
```
|
||||
Set the border width
|
||||
|
||||
Usage: komorebic.exe border-width <WIDTH>
|
||||
|
||||
Arguments:
|
||||
<WIDTH>
|
||||
Desired width of the window border
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,9 +1,9 @@
|
||||
# active-window-border
|
||||
# border
|
||||
|
||||
```
|
||||
Enable or disable the active window border
|
||||
Enable or disable borders
|
||||
|
||||
Usage: komorebic.exe active-window-border <BOOLEAN_STATE>
|
||||
Usage: komorebic.exe border <BOOLEAN_STATE>
|
||||
|
||||
Arguments:
|
||||
<BOOLEAN_STATE>
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
|
||||
|
||||
Arguments:
|
||||
<DEFAULT_LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
12
docs/cli/clear-all-workspace-rules.md
Normal file
12
docs/cli/clear-all-workspace-rules.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# clear-all-workspace-rules
|
||||
|
||||
```
|
||||
Remove all application association rules for all workspaces
|
||||
|
||||
Usage: komorebic.exe clear-all-workspace-rules
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/clear-named-workspace-rules.md
Normal file
16
docs/cli/clear-named-workspace-rules.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# clear-named-workspace-rules
|
||||
|
||||
```
|
||||
Remove all application association rules for a named workspace
|
||||
|
||||
Usage: komorebic.exe clear-named-workspace-rules <WORKSPACE>
|
||||
|
||||
Arguments:
|
||||
<WORKSPACE>
|
||||
Name of a workspace
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
19
docs/cli/clear-workspace-rules.md
Normal file
19
docs/cli/clear-workspace-rules.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# clear-workspace-rules
|
||||
|
||||
```
|
||||
Remove all application association rules for a workspace by monitor and workspace index
|
||||
|
||||
Usage: komorebic.exe clear-workspace-rules <MONITOR> <WORKSPACE>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -10,6 +10,7 @@ Arguments:
|
||||
Possible values:
|
||||
- swap: Swap the window container with the window container at the edge of the adjacent monitor
|
||||
- insert: Insert the window container into the focused workspace on the adjacent monitor
|
||||
- no-op: Do nothing if trying to move a window container in the direction of an adjacent monitor
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
16
docs/cli/cycle-move-workspace-to-monitor.md
Normal file
16
docs/cli/cycle-move-workspace-to-monitor.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# cycle-move-workspace-to-monitor
|
||||
|
||||
```
|
||||
Move the focused workspace monitor in the given cycle direction
|
||||
|
||||
Usage: komorebic.exe cycle-move-workspace-to-monitor <CYCLE_DIRECTION>
|
||||
|
||||
Arguments:
|
||||
<CYCLE_DIRECTION>
|
||||
[possible values: previous, next]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/global-state.md
Normal file
12
docs/cli/global-state.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# global-state
|
||||
|
||||
```
|
||||
Show a JSON representation of the current global state
|
||||
|
||||
Usage: komorebic.exe global-state
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/gui.md
Normal file
12
docs/cli/gui.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# gui
|
||||
|
||||
```
|
||||
Launch the komorebi-gui debugging tool
|
||||
|
||||
Usage: komorebic.exe gui
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/monitor-information.md
Normal file
12
docs/cli/monitor-information.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# monitor-information
|
||||
|
||||
```
|
||||
Show information about connected monitors
|
||||
|
||||
Usage: komorebic.exe monitor-information
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
19
docs/cli/move-to-monitor-workspace.md
Normal file
19
docs/cli/move-to-monitor-workspace.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# move-to-monitor-workspace
|
||||
|
||||
```
|
||||
Move the focused window to the specified monitor workspace
|
||||
|
||||
Usage: komorebic.exe move-to-monitor-workspace <TARGET_MONITOR> <TARGET_WORKSPACE>
|
||||
|
||||
Arguments:
|
||||
<TARGET_MONITOR>
|
||||
Target monitor index (zero-indexed)
|
||||
|
||||
<TARGET_WORKSPACE>
|
||||
Workspace index on the target monitor (zero-indexed)
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -10,7 +10,7 @@ Arguments:
|
||||
Target workspace name
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
16
docs/cli/promote-window.md
Normal file
16
docs/cli/promote-window.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# promote-window
|
||||
|
||||
```
|
||||
Promote the window in the specified direction
|
||||
|
||||
Usage: komorebic.exe promote-window <OPERATION_DIRECTION>
|
||||
|
||||
Arguments:
|
||||
<OPERATION_DIRECTION>
|
||||
[possible values: left, right, up, down]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/stack-all.md
Normal file
12
docs/cli/stack-all.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# stack-all
|
||||
|
||||
```
|
||||
Stack all windows on the focused workspace
|
||||
|
||||
Usage: komorebic.exe stack-all
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/transparency-alpha.md
Normal file
16
docs/cli/transparency-alpha.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# transparency-alpha
|
||||
|
||||
```
|
||||
Set the alpha value for unfocused window transparency
|
||||
|
||||
Usage: komorebic.exe transparency-alpha <ALPHA>
|
||||
|
||||
Arguments:
|
||||
<ALPHA>
|
||||
Alpha
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/transparency.md
Normal file
16
docs/cli/transparency.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# transparency
|
||||
|
||||
```
|
||||
Enable or disable transparency for unfocused windows
|
||||
|
||||
Usage: komorebic.exe transparency <BOOLEAN_STATE>
|
||||
|
||||
Arguments:
|
||||
<BOOLEAN_STATE>
|
||||
[possible values: enable, disable]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/unstack-all.md
Normal file
12
docs/cli/unstack-all.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# unstack-all
|
||||
|
||||
```
|
||||
Unstack all windows in the focused container
|
||||
|
||||
Usage: komorebic.exe unstack-all
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -10,7 +10,7 @@ Arguments:
|
||||
Possible values:
|
||||
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
|
||||
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -16,7 +16,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Active Window Border
|
||||
|
||||
If you would like to add a visual border around the currently focused window,
|
||||
ensure the following options are defined in the `komorebi.json` configuration
|
||||
file.
|
||||
|
||||
```json
|
||||
{
|
||||
"active_window_border": true,
|
||||
"active_window_border_style": "Rounded",
|
||||
"active_window_border_colours": {
|
||||
"single": {
|
||||
"r": 66,
|
||||
"g": 165,
|
||||
"b": 245
|
||||
},
|
||||
"stack": {
|
||||
"r": 256,
|
||||
"g": 165,
|
||||
"b": 66
|
||||
},
|
||||
"monocle": {
|
||||
"r": 255,
|
||||
"g": 51,
|
||||
"b": 153
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
It is important to note that the active window border will only apply to
|
||||
windows managed by `komorebi`.
|
||||
|
||||
This feature is not considered stable, and you may encounter visual artifacts
|
||||
from time to time.
|
||||
|
||||
[](https://www.youtube.com/watch?v=7_9D22t7KK4)
|
||||
25
docs/common-workflows/animations.md
Normal file
25
docs/common-workflows/animations.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Animations
|
||||
|
||||
If you would like to add window movement animations, ensure the following options are
|
||||
defined in the `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"animation": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Window movement animations only apply to actions taking place within the same monitor
|
||||
workspace.
|
||||
|
||||
You can optionally set a custom duration in ms with `animation.duration` (default: `250`),
|
||||
a custom style with `animation.style` (default: `Linear`), and a custom FPS value with
|
||||
`animation.fps` (default: `60`).
|
||||
|
||||
It is important to note that higher `fps` and a longer `duration` settings will result
|
||||
in increased CPU usage.
|
||||
|
||||
This feature is not considered stable, and you may encounter visual artifacts
|
||||
from time to time.
|
||||
@@ -1,6 +1,4 @@
|
||||
# AutoHotKey
|
||||
|
||||
<!-- TODO: Update this completely -->
|
||||
# AutoHotkey
|
||||
|
||||
If you would like to use Autohotkey, please make sure you have AutoHotKey v2
|
||||
installed.
|
||||
@@ -9,22 +7,16 @@ Generally, users who opt for AHK will have specific needs that can only be
|
||||
addressed by the advanced functionality of AHK, and so they are assumed to be
|
||||
able to craft their own configuration files.
|
||||
|
||||
If you would like to try out AHK, a simple sample configuration powered by
|
||||
`komorebic.lib.ahk` is provided as a starting point. This sample configuration
|
||||
does not take into account the use of a static configuration file; if you
|
||||
choose to use a static configuration file alongside AHK, you can remove all the
|
||||
configuration options from your `komorebi.ahk` and use it solely to handle
|
||||
hotkey bindings.
|
||||
If you would like to try out AHK, here is a simple sample configuration which
|
||||
largely matches the `whkdrc` sample configuration.
|
||||
|
||||
|
||||
```powershell
|
||||
# save the latest generated komorebic library to ~/komorebic.lib.ahk
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebic.lib.ahk -OutFile $Env:USERPROFILE\komorebic.lib.ahk
|
||||
|
||||
# save the latest generated app-specific config tweaks and fixes to ~/komorebi.generated.ahk
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebi.generated.ahk -OutFile $Env:USERPROFILE\komorebi.generated.ahk
|
||||
|
||||
# save the sample komorebi configuration file to ~/komorebi.ahk
|
||||
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebi.sample.ahk -OutFile $Env:USERPROFILE\komorebi.ahk
|
||||
```autohotkey
|
||||
{% include "./komorebi.ahk.txt" %}
|
||||
```
|
||||
|
||||
By default, the `komorebi.ahk` file should be located in the `$Env:USERPROFILE`
|
||||
directory, however, if `$Env:KOMOREBI_CONFIG_HOME` is set, it should be located
|
||||
there.
|
||||
|
||||
Once the file is in place, you can stop komorebi and whkd by running `komorebic stop --whkd`,
|
||||
and then start komorebi with Autohotkey by running `komorebic start --ahk`.
|
||||
|
||||
28
docs/common-workflows/borders.md
Normal file
28
docs/common-workflows/borders.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Borders
|
||||
|
||||
If you would like to add a visual border around both the currently focused window
|
||||
and unfocused windows ensure the following options are defined in the `komorebi.json`
|
||||
configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"border": true,
|
||||
"border_width": 8,
|
||||
"border_offset": -1,
|
||||
"border_style": "System",
|
||||
"border_colours": {
|
||||
"single": "#42a5f5",
|
||||
"stack": "#00a542",
|
||||
"monocle": "#ff3399",
|
||||
"unfocused": "#808080"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It is important to note that borders will only apply to windows managed by `komorebi`.
|
||||
|
||||
This feature is not considered stable, and you may encounter visual artifacts
|
||||
from time to time.
|
||||
|
||||
[](https://www.youtube.com/watch?v=7_9D22t7KK4)
|
||||
@@ -1,4 +1,4 @@
|
||||
# Dynamically Layout Switching
|
||||
# Dynamic Layout Switching
|
||||
|
||||
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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
❗️**NOTE**: A significant number of force-manage window rules for the most
|
||||
common applications are [already generated for
|
||||
you](https://github.com/LGUG2Z/komorebi/#generating-common-application-specific-configurations)
|
||||
you](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
|
||||
In some rare cases, a window may not automatically be registered to be managed
|
||||
by `komorebi`. You can add rules to enforce this behaviour in the
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
❗️**NOTE**: A significant number of ignored window rules for the most common
|
||||
applications are [already generated for
|
||||
you](https://github.com/LGUG2Z/komorebi/#generating-common-application-specific-configurations)
|
||||
you](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
|
||||
Sometimes you will want a specific application to never be tiled, and instead
|
||||
float all the time. You can add rules to enforce this behaviour in the
|
||||
|
||||
17
docs/common-workflows/set-display-index.md
Normal file
17
docs/common-workflows/set-display-index.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Setting a Given Display to a Specific Index
|
||||
|
||||
If you would like `komorebi` to remember monitor index positions, you will need to set the `display_index_preferences`
|
||||
configuration option in the static configuration file.
|
||||
|
||||
Display IDs can be found using `komorebic monitor-information`.
|
||||
|
||||
Then, in `komorebi.json`, you simply need to specify the preferred index position for each display ID:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "DEL4310-5&1a6c0954&0&UID209155",
|
||||
"1": "<another-display_id>"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -101,6 +101,16 @@ monocle.
|
||||
+-------+-----+
|
||||
```
|
||||
|
||||
#### RightMainVerticalStack
|
||||
|
||||
```
|
||||
+-----+-------+
|
||||
| | |
|
||||
+-----+ |
|
||||
| | |
|
||||
+-----+-------+
|
||||
```
|
||||
|
||||
#### Horizontal Stack
|
||||
|
||||
```
|
||||
@@ -122,6 +132,7 @@ monocle.
|
||||
```
|
||||
|
||||
#### Rows
|
||||
|
||||
If you have a vertical monitor, I recommend using this layout.
|
||||
|
||||
```
|
||||
@@ -133,6 +144,7 @@ If you have a vertical monitor, I recommend using this layout.
|
||||
```
|
||||
|
||||
#### Ultrawide Vertical Stack
|
||||
|
||||
If you have an ultrawide monitor, I recommend using this layout.
|
||||
|
||||
```
|
||||
@@ -146,6 +158,7 @@ If you have an ultrawide monitor, I recommend using this layout.
|
||||
```
|
||||
|
||||
### Grid
|
||||
|
||||
If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layouts) this is almost exactly the same!
|
||||
|
||||
```
|
||||
@@ -183,7 +196,8 @@ which shell you use in your terminal.
|
||||
* `powershell` - set this if you are using the version of PowerShell that comes
|
||||
installed with Windows 10+ (the executable file for this is `powershell.exe`)
|
||||
|
||||
* `pwsh` - set this if you are using PowerShell 7+, which you have installed yourself either through the Windows Store or WinGet (the executable file for this is `pwsh.exe`)
|
||||
* `pwsh` - set this if you are using PowerShell 7+, which you have installed yourself either through the Windows Store
|
||||
or WinGet (the executable file for this is `pwsh.exe`)
|
||||
|
||||
* `cmd` - set this if you don't want to use PowerShell at all and instead you
|
||||
want to call commands through the shell used by the old-school Command
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Getting started
|
||||
|
||||
`komorebi` is a tiling window manager for Windows that is comprised comprised
|
||||
of two main binaries, `komorebi.exe`, which contains the window manager itself,
|
||||
`komorebi` is a tiling window manager for Windows that is comprised of two
|
||||
main binaries, `komorebi.exe`, which contains the window manager itself,
|
||||
and `komorebic.exe`, which is the main way to send commands to the tiling
|
||||
window manager.
|
||||
|
||||
It is important to note that neither `komorebi.exe` or `komorebic.exe` handle
|
||||
It is important to note that neither `komorebi.exe` nor `komorebic.exe` handle
|
||||
key bindings, because `komorebi` is a tiling window manager and not a hotkey
|
||||
daemon.
|
||||
|
||||
@@ -27,17 +27,17 @@ to manipulate the window manager, you use
|
||||
|
||||
`komorebi` is available pre-built to install via
|
||||
[Scoop](https://scoop.sh/#/apps?q=komorebi) and
|
||||
[WinGet](https://winget.run/pkg/LGUG2Z/komorebi), and you may also built
|
||||
[WinGet](https://winget.run/pkg/LGUG2Z/komorebi), and you may also build
|
||||
it from [source](https://github.com/LGUG2Z/komorebi) if you would prefer.
|
||||
|
||||
- [Scoop](#scoop)
|
||||
- [WinGet](#winget)
|
||||
- [Building from source](#building-from-source)
|
||||
- [Offline](#offline)
|
||||
- [Scoop](#scoop)
|
||||
- [WinGet](#winget)
|
||||
- [Building from source](#building-from-source)
|
||||
- [Offline](#offline)
|
||||
|
||||
## Long path support
|
||||
|
||||
It highly recommended that you enable support for long paths in Windows by
|
||||
It is highly recommended that you enable support for long paths in Windows by
|
||||
running the following command in an Administrator Terminal before installing
|
||||
`komorebi`.
|
||||
|
||||
@@ -45,6 +45,12 @@ running the following command in an Administrator Terminal before installing
|
||||
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
|
||||
```
|
||||
|
||||
## Disabling unnecessary system animations
|
||||
|
||||
It is highly recommended that you enable the "Turn off all unnecessary animations (when possible)" option in
|
||||
"Control Panel > Ease of Access > Ease of Access Centre / Make the computer easier to see" for the best performance with
|
||||
komorebi.
|
||||
|
||||
## Scoop
|
||||
|
||||
Make sure you have installed [`scoop`](https://scoop.sh) and verified that
|
||||
@@ -108,6 +114,7 @@ Clone the git repository, enter the directory, and build the following binaries:
|
||||
cargo +stable install --path komorebi --locked
|
||||
cargo +stable install --path komorebic --locked
|
||||
cargo +stable install --path komorebic-no-console --locked
|
||||
cargo +stable install --path komorebi-gui --locked
|
||||
```
|
||||
|
||||
If the binaries have been built and added to your `$PATH` correctly, you should
|
||||
@@ -121,3 +128,21 @@ an offline machine to install.
|
||||
|
||||
Once installed, proceed to get the [example configurations](example-configurations.md) (none of the commands for
|
||||
first-time set up and running komorebi require an internet connection).
|
||||
|
||||
## Uninstallation
|
||||
|
||||
Before uninstalling, first run `komorebic stop --whkd` to make sure that both
|
||||
the `komorebi` and `whkd` processes have been stopped.
|
||||
|
||||
Then, depending on whether you installed with Scoop or WinGet, run `scoop
|
||||
uninstall komorebi whkd` or `winget uninstall LGUG2Z.komorebi LGUG2Z.whkd`.
|
||||
|
||||
Finally, you can run the following commands in a PowerShell prompt to clean up
|
||||
files created by the `quickstart` command and any other runtime files:
|
||||
|
||||
```powershell
|
||||
rm $Env:USERPROFILE\komorebi.json
|
||||
rm $Env:USERPROFILE\applications.yaml
|
||||
rm $Env:USERPROFILE\.config\whkdrc
|
||||
rm -r -Force $Env:LOCALAPPDATA\komorebi
|
||||
```
|
||||
|
||||
71
docs/komorebi.ahk.txt
Normal file
71
docs/komorebi.ahk.txt
Normal file
@@ -0,0 +1,71 @@
|
||||
#Requires AutoHotkey v2.0.2
|
||||
#SingleInstance Force
|
||||
|
||||
Komorebic(cmd) {
|
||||
RunWait(format("komorebic.exe {}", cmd), , "Hide")
|
||||
}
|
||||
|
||||
!q::Komorebic("close")
|
||||
!m::Komorebic("minimize")
|
||||
|
||||
; Focus windows
|
||||
!h::Komorebic("focus left")
|
||||
!j::Komorebic("focus down")
|
||||
!k::Komorebic("focus up")
|
||||
!l::Komorebic("focus right")
|
||||
|
||||
!+[::Komorebic("cycle-focus previous")
|
||||
!+]::Komorebic("cycle-focus next")
|
||||
|
||||
; Move windows
|
||||
!+h::Komorebic("move left")
|
||||
!+j::Komorebic("move down")
|
||||
!+k::Komorebic("move up")
|
||||
!+l::Komorebic("move right")
|
||||
|
||||
; Stack windows
|
||||
!Left::Komorebic("stack left")
|
||||
!Down::Komorebic("stack down")
|
||||
!Up::Komorebic("stack up")
|
||||
!Right::Komorebic("stack right")
|
||||
!;::Komorebic("unstack")
|
||||
![::Komorebic("cycle-stack previous")
|
||||
!]::Komorebic("cycle-stack next")
|
||||
|
||||
; Resize
|
||||
!=::Komorebic("resize-axis horizontal increase")
|
||||
!-::Komorebic("resize-axis horizontal decrease")
|
||||
!+=::Komorebic("resize-axis vertical increase")
|
||||
!+_::Komorebic("resize-axis vertical decrease")
|
||||
|
||||
; Manipulate windows
|
||||
!t::Komorebic("toggle-float")
|
||||
!f::Komorebic("toggle-monocle")
|
||||
|
||||
; Window manager options
|
||||
!+r::Komorebic("retile")
|
||||
!p::Komorebic("toggle-pause")
|
||||
|
||||
; Layouts
|
||||
!x::Komorebic("flip-layout horizontal")
|
||||
!y::Komorebic("flip-layout vertical")
|
||||
|
||||
; Workspaces
|
||||
!1::Komorebic("focus-workspace 0")
|
||||
!2::Komorebic("focus-workspace 1")
|
||||
!3::Komorebic("focus-workspace 2")
|
||||
!4::Komorebic("focus-workspace 3")
|
||||
!5::Komorebic("focus-workspace 4")
|
||||
!6::Komorebic("focus-workspace 5")
|
||||
!7::Komorebic("focus-workspace 6")
|
||||
!8::Komorebic("focus-workspace 7")
|
||||
|
||||
; Move windows across workspaces
|
||||
!+1::Komorebic("move-to-workspace 0")
|
||||
!+2::Komorebic("move-to-workspace 1")
|
||||
!+3::Komorebic("move-to-workspace 2")
|
||||
!+4::Komorebic("move-to-workspace 3")
|
||||
!+5::Komorebic("move-to-workspace 4")
|
||||
!+6::Komorebic("move-to-workspace 5")
|
||||
!+7::Komorebic("move-to-workspace 6")
|
||||
!+8::Komorebic("move-to-workspace 7")
|
||||
@@ -1,17 +1,28 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.24/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.28/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
"default_workspace_padding": 20,
|
||||
"default_container_padding": 20,
|
||||
"border_padding": 8,
|
||||
"border": true,
|
||||
"border_width": 8,
|
||||
"border_offset": -1,
|
||||
"active_window_border": false,
|
||||
"active_window_border_colours": {
|
||||
"border_colours": {
|
||||
"single": "#42a5f5",
|
||||
"stack": "#00a542",
|
||||
"monocle": "#ff3399"
|
||||
"monocle": "#ff3399",
|
||||
"unfocused": "#808080"
|
||||
},
|
||||
"stackbar": {
|
||||
"height": 40,
|
||||
"mode": "OnStack",
|
||||
"tabs": {
|
||||
"width": 300,
|
||||
"focused_text": "#00a542",
|
||||
"unfocused_text": "#b3b3b3",
|
||||
"background": "#141414"
|
||||
}
|
||||
},
|
||||
"monitors": [
|
||||
{
|
||||
@@ -35,6 +46,14 @@
|
||||
{
|
||||
"name": "V",
|
||||
"layout": "Rows"
|
||||
},
|
||||
{
|
||||
"name": "VI",
|
||||
"layout": "Grid"
|
||||
},
|
||||
{
|
||||
"name": "VII",
|
||||
"layout": "RightMainVerticalStack"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# v0.1.22
|
||||
|
||||
In addition to the [changelog](https://github.com/LGUG2Z/komorebi/releases/tag/v0.1.22) of new features and fixes,
|
||||
please note the following changes from `v0.1.21` to adjust your configuration files accordingly.
|
||||
|
||||
@@ -49,8 +51,8 @@ A 1px border is drawn around the window edge. Users may see a gap for a single p
|
||||
transparent edge - this is the windows themed edge, and is not present for all applications.
|
||||
|
||||
```json
|
||||
{
|
||||
{
|
||||
"border_offset": 0,
|
||||
"border_width": 1
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
124
docs/troubleshooting.md
Normal file
124
docs/troubleshooting.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Troubleshooting
|
||||
|
||||
## AutoHotKey executable not found
|
||||
|
||||
If you try to start komorebi with AHK using `komorebic start --ahk`, and you have
|
||||
not installed AHK using `scoop`, you'll probably receive an error:
|
||||
|
||||
```text
|
||||
Error: could not find autohotkey, please make sure it is installed before using the --ahk flag
|
||||
```
|
||||
|
||||
Depending on how AHK is installed the executable on your system may have a
|
||||
different name. In order to account for this, you may set the `KOMOREBI_AHK_EXE`
|
||||
environment variable in your
|
||||
[PowerShell profile](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7.4)
|
||||
to match the name of the executable as it is found on your system.
|
||||
|
||||
After setting `KOMOREBI_AHK_EXE` make sure to either reload your PowerShell
|
||||
profile or open a new terminal tab.
|
||||
|
||||
## Komorebi is unresponsive when the display wakes from sleep
|
||||
|
||||
This can happen in rare cases when your monitor state is not preserved after it
|
||||
wakes from sleep.
|
||||
|
||||
### Problem
|
||||
|
||||
Your hotkeys in _whkd_ work, but it feels as if _komorebi_ knows nothing about
|
||||
the previous state (you can't control previous windows, although newly launched ones
|
||||
can be manipulated as normal).
|
||||
|
||||
### Solution
|
||||
|
||||
Some monitors, such as the Samsung G8/G9 (LED, Neo, OLED) have an _adaptive
|
||||
sync_ or _variable refresh rate_ setting within the actual monitor OSD that can
|
||||
disrupt how the device is persisted in the _komorebi_ state following suspension.
|
||||
|
||||
To fix this, please try to disable _Adaptive Sync_ or any other _VRR_ branded
|
||||
alias by referring to the manufacturer's documentation.
|
||||
|
||||
!!! warning
|
||||
|
||||
Disabling VRR within Windows (e.g. _Nvidia Control Panel_) may work and can indeed
|
||||
change the configuration you see within your monitor's OSD, but some monitors
|
||||
will re-enable the setting regardless following suspension.
|
||||
|
||||
### Reproducing
|
||||
|
||||
Ensure _komorebi_ is in an operational state by executing `komorebic start` as
|
||||
normal.
|
||||
|
||||
If _komorebi_ is already unresponsive, then please restart _komorebi_ first by
|
||||
running `komorebic stop` and `komorebic start`.
|
||||
|
||||
1. **`komorebic state`**
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": {
|
||||
"elements": [
|
||||
{
|
||||
"id": 65537,
|
||||
"name": "DISPLAY1",
|
||||
"device": "SAM71AA",
|
||||
"device_id": "SAM71AA-5&a1a3e88&0&UID24834",
|
||||
"size": {
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"right": 5120,
|
||||
"bottom": 1440
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This appears to be fine -- _komorebi_ is aware of the device and associated
|
||||
window handles.
|
||||
|
||||
2. **Let your display go to sleep.**
|
||||
|
||||
Simply turning the monitor off is not enough to reproduce the problem; you must
|
||||
let Windows turn off the display itself.
|
||||
|
||||
To avoid waiting an eternity:
|
||||
|
||||
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
|
||||
Settings_
|
||||
|
||||
_Turn off the display: 1 minute_
|
||||
|
||||
Allow a minute for the display to reset, then once it actually shuts off
|
||||
allow for any additional time as prompted by your monitor for the cycle to
|
||||
complete.
|
||||
|
||||
3. **Wake your display again** by pressing any key.
|
||||
|
||||
_komorebi_ should now be unresponsive.
|
||||
|
||||
4. **`komorebic state`**
|
||||
|
||||
Don't stop _komorebi_ just yet.
|
||||
|
||||
Since it's unresponsive, you can open another shell instead to execute the above command.
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": {
|
||||
"elements": [
|
||||
{
|
||||
"id": 65537,
|
||||
"name": "DISPLAY1",
|
||||
"device": null,
|
||||
"device_id": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can see the _komorebi_ state is no longer associated with the previous
|
||||
device: `null`, suggesting an issue when the display resumes from a suspended
|
||||
state.
|
||||
@@ -10,6 +10,9 @@ alt + shift + o : komorebic reload-configuration
|
||||
# alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox }
|
||||
# alt + b : if ($wshell.AppActivate('Chrome') -eq $False) { start chrome }
|
||||
|
||||
alt + q : komorebic close
|
||||
alt + m : komorebic minimize
|
||||
|
||||
# Focus windows
|
||||
alt + h : komorebic focus left
|
||||
alt + j : komorebic focus down
|
||||
@@ -56,8 +59,18 @@ alt + y : komorebic flip-layout vertical
|
||||
alt + 1 : komorebic focus-workspace 0
|
||||
alt + 2 : komorebic focus-workspace 1
|
||||
alt + 3 : komorebic focus-workspace 2
|
||||
alt + 4 : komorebic focus-workspace 3
|
||||
alt + 5 : komorebic focus-workspace 4
|
||||
alt + 6 : komorebic focus-workspace 5
|
||||
alt + 7 : komorebic focus-workspace 6
|
||||
alt + 8 : komorebic focus-workspace 7
|
||||
|
||||
# Move windows across workspaces
|
||||
alt + shift + 1 : komorebic move-to-workspace 0
|
||||
alt + shift + 2 : komorebic move-to-workspace 1
|
||||
alt + shift + 3 : komorebic move-to-workspace 2
|
||||
alt + shift + 4 : komorebic move-to-workspace 3
|
||||
alt + shift + 5 : komorebic move-to-workspace 4
|
||||
alt + shift + 6 : komorebic move-to-workspace 5
|
||||
alt + shift + 7 : komorebic move-to-workspace 6
|
||||
alt + shift + 8 : komorebic move-to-workspace 7
|
||||
|
||||
8
justfile
8
justfile
@@ -16,19 +16,12 @@ fmt:
|
||||
install-target target:
|
||||
cargo +stable install --path {{ target }} --locked
|
||||
|
||||
prepare:
|
||||
komorebic ahk-asc '~/.config/komorebi/applications.yaml'
|
||||
komorebic pwsh-asc '~/.config/komorebi/applications.yaml'
|
||||
cat '~/.config/komorebi/komorebi.generated.ps1' >komorebi.generated.ps1
|
||||
cat '~/.config/komorebi/komorebi.generated.ahk' >komorebi.generated.ahk
|
||||
|
||||
install:
|
||||
just install-target komorebic
|
||||
just install-target komorebic-no-console
|
||||
just install-target komorebi
|
||||
|
||||
run:
|
||||
just install-target komorebic
|
||||
cargo +stable run --bin komorebi --locked
|
||||
|
||||
warn $RUST_LOG="warn":
|
||||
@@ -44,7 +37,6 @@ trace $RUST_LOG="trace":
|
||||
just run
|
||||
|
||||
deadlock $RUST_LOG="trace":
|
||||
just install-komorebic
|
||||
cargo +stable run --bin komorebi --locked --features deadlock_detection
|
||||
|
||||
docgen:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.25-dev.0"
|
||||
version = "0.1.28-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::colour::Colour;
|
||||
pub use komorebi::colour::Rgb;
|
||||
pub use komorebi::container::Container;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
pub use komorebi::ring::Ring;
|
||||
pub use komorebi::window::Window;
|
||||
pub use komorebi::window_manager_event::WindowManagerEvent;
|
||||
pub use komorebi::workspace::Workspace;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::GlobalState;
|
||||
pub use komorebi::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::RuleDebug;
|
||||
pub use komorebi::StackbarConfig;
|
||||
pub use komorebi::State;
|
||||
pub use komorebi::StaticConfig;
|
||||
pub use komorebi::TabsConfig;
|
||||
pub use komorebi_core::Arrangement;
|
||||
pub use komorebi_core::Axis;
|
||||
pub use komorebi_core::BorderStyle;
|
||||
pub use komorebi_core::CustomLayout;
|
||||
pub use komorebi_core::CycleDirection;
|
||||
pub use komorebi_core::DefaultLayout;
|
||||
@@ -20,6 +29,9 @@ pub use komorebi_core::Layout;
|
||||
pub use komorebi_core::OperationDirection;
|
||||
pub use komorebi_core::Rect;
|
||||
pub use komorebi_core::SocketMessage;
|
||||
pub use komorebi_core::StackbarLabel;
|
||||
pub use komorebi_core::StackbarMode;
|
||||
pub use komorebi_core::WindowKind;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.25-dev.0"
|
||||
version = "0.1.28-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
42
komorebi-core/src/animation.rs
Normal file
42
komorebi-core/src/animation.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use clap::ValueEnum;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
pub enum AnimationStyle {
|
||||
Linear,
|
||||
EaseInSine,
|
||||
EaseOutSine,
|
||||
EaseInOutSine,
|
||||
EaseInQuad,
|
||||
EaseOutQuad,
|
||||
EaseInOutQuad,
|
||||
EaseInCubic,
|
||||
EaseInOutCubic,
|
||||
EaseInQuart,
|
||||
EaseOutQuart,
|
||||
EaseInOutQuart,
|
||||
EaseInQuint,
|
||||
EaseOutQuint,
|
||||
EaseInOutQuint,
|
||||
EaseInExpo,
|
||||
EaseOutExpo,
|
||||
EaseInOutExpo,
|
||||
EaseInCirc,
|
||||
EaseOutCirc,
|
||||
EaseInOutCirc,
|
||||
EaseInBack,
|
||||
EaseOutBack,
|
||||
EaseInOutBack,
|
||||
EaseInElastic,
|
||||
EaseOutElastic,
|
||||
EaseInOutElastic,
|
||||
EaseInBounce,
|
||||
EaseOutBounce,
|
||||
EaseInOutBounce,
|
||||
}
|
||||
@@ -26,7 +26,7 @@ pub trait Arrangement {
|
||||
}
|
||||
|
||||
impl Arrangement for DefaultLayout {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
@@ -44,8 +44,56 @@ impl Arrangement for DefaultLayout {
|
||||
layout_flip,
|
||||
calculate_resize_adjustments(resize_dimensions),
|
||||
),
|
||||
Self::Columns => columns(area, len),
|
||||
Self::Rows => rows(area, len),
|
||||
Self::Columns => {
|
||||
let mut layouts = columns(area, len);
|
||||
|
||||
let adjustment = calculate_columns_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
columns_reverse(&mut layouts);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::Rows => {
|
||||
let mut layouts = rows(area, len);
|
||||
|
||||
let adjustment = calculate_rows_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
rows_reverse(&mut layouts);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::VerticalStack => {
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
|
||||
@@ -54,16 +102,8 @@ impl Arrangement for DefaultLayout {
|
||||
_ => 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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let main_left = area.left;
|
||||
let stack_left = area.left + primary_right;
|
||||
|
||||
if len >= 1 {
|
||||
layouts.push(Rect {
|
||||
@@ -86,6 +126,113 @@ impl Arrangement for DefaultLayout {
|
||||
}
|
||||
}
|
||||
|
||||
let adjustment = calculate_vertical_stack_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left;
|
||||
}
|
||||
primary.left = rest[0].left + rest[0].right;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 3.. = len {
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::RightMainVerticalStack => {
|
||||
// Shamelessly borrowed from LeftWM: https://github.com/leftwm/leftwm/commit/f673851745295ae7584a102535566f559d96a941
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
|
||||
let primary_width = match len {
|
||||
1 => area.right,
|
||||
_ => area.right / 2,
|
||||
};
|
||||
|
||||
let primary_left = match len {
|
||||
1 => 0,
|
||||
_ => area.right - primary_width,
|
||||
};
|
||||
|
||||
if len >= 1 {
|
||||
layouts.push(Rect {
|
||||
left: area.left + primary_left,
|
||||
top: area.top,
|
||||
right: primary_width,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
|
||||
if len > 1 {
|
||||
layouts.append(&mut rows(
|
||||
&Rect {
|
||||
left: area.left,
|
||||
top: area.top,
|
||||
right: primary_left,
|
||||
bottom: area.bottom,
|
||||
},
|
||||
len - 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let adjustment = calculate_right_vertical_stack_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
primary.left = rest[0].left;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left + primary.right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 3.. = len {
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::HorizontalStack => {
|
||||
@@ -96,16 +243,8 @@ impl Arrangement for DefaultLayout {
|
||||
_ => 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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let main_top = area.top;
|
||||
let stack_top = area.top + bottom;
|
||||
|
||||
if len >= 1 {
|
||||
layouts.push(Rect {
|
||||
@@ -128,9 +267,155 @@ impl Arrangement for DefaultLayout {
|
||||
}
|
||||
}
|
||||
|
||||
let adjustment = calculate_horizontal_stack_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
for rect in rest.iter_mut() {
|
||||
rect.top = primary.top;
|
||||
}
|
||||
primary.top = rest[0].top + rest[0].bottom;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 3.. = len {
|
||||
columns_reverse(&mut layouts[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 primary = area.left + secondary_right;
|
||||
let secondary = area.left;
|
||||
|
||||
(primary, secondary, 0)
|
||||
}
|
||||
_ => {
|
||||
let primary = area.left + secondary_right;
|
||||
let secondary = area.left;
|
||||
let stack = area.left + primary_right + secondary_right;
|
||||
|
||||
(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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let adjustment = calculate_ultrawide_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
match len {
|
||||
2 => {
|
||||
let (primary, secondary) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
let secondary = &mut secondary[0];
|
||||
|
||||
primary.left = secondary.left;
|
||||
secondary.left = primary.left + primary.right;
|
||||
}
|
||||
3.. => {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let (secondary, tertiary) = rest.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
let secondary = &mut secondary[0];
|
||||
|
||||
for rect in tertiary.iter_mut() {
|
||||
rect.left = secondary.left;
|
||||
}
|
||||
primary.left = tertiary[0].left + tertiary[0].right;
|
||||
secondary.left = primary.left + primary.right;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 4.. = len {
|
||||
rows_reverse(&mut layouts[2..]);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::UltrawideVerticalStack => ultrawide(area, len, layout_flip, resize_dimensions),
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_truncation,
|
||||
@@ -319,9 +604,17 @@ impl Arrangement for CustomLayout {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Axis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
@@ -370,6 +663,22 @@ fn rows(area: &Rect, len: usize) -> Vec<Rect> {
|
||||
layouts
|
||||
}
|
||||
|
||||
fn columns_reverse(columns: &mut [Rect]) {
|
||||
let len = columns.len();
|
||||
columns[len - 1].left = columns[0].left;
|
||||
for i in (0..len - 1).rev() {
|
||||
columns[i].left = columns[i + 1].left + columns[i + 1].right;
|
||||
}
|
||||
}
|
||||
|
||||
fn rows_reverse(rows: &mut [Rect]) {
|
||||
let len = rows.len();
|
||||
rows[len - 1].top = rows[0].top;
|
||||
for i in (0..len - 1).rev() {
|
||||
rows[i].top = rows[i + 1].top + rows[i + 1].bottom;
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
|
||||
let mut resize_adjustments = resize_dimensions.to_vec();
|
||||
|
||||
@@ -563,6 +872,187 @@ fn recursive_fibonacci(
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_columns_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
match len {
|
||||
0 | 1 => (),
|
||||
_ => {
|
||||
for (i, rect) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
if i != 0 {
|
||||
resize_right(&mut result[i - 1], rect.left);
|
||||
resize_left(&mut result[i], rect.left);
|
||||
}
|
||||
|
||||
if i != len - 1 {
|
||||
resize_right(&mut result[i], rect.right);
|
||||
resize_left(&mut result[i + 1], rect.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_rows_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
match len {
|
||||
0 | 1 => (),
|
||||
_ => {
|
||||
for (i, rect) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
if i != 0 {
|
||||
resize_bottom(&mut result[i - 1], rect.top);
|
||||
resize_top(&mut result[i], rect.top);
|
||||
}
|
||||
|
||||
if i != len - 1 {
|
||||
resize_bottom(&mut result[i], rect.bottom);
|
||||
resize_top(&mut result[i + 1], rect.bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_vertical_stack_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
match len {
|
||||
// One container can't be resized
|
||||
0 | 1 => (),
|
||||
_ => {
|
||||
let (master, stack) = result.split_at_mut(1);
|
||||
let primary = &mut master[0];
|
||||
|
||||
if let Some(resize) = resize_dimensions[0] {
|
||||
resize_right(primary, resize.right);
|
||||
for s in &mut *stack {
|
||||
resize_left(s, resize.right);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stack on the right
|
||||
for (i, rect) in resize_dimensions[1..].iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
resize_right(primary, rect.left);
|
||||
stack
|
||||
.iter_mut()
|
||||
.for_each(|vertical_element| resize_left(vertical_element, rect.left));
|
||||
|
||||
// Containers in stack except first can be resized up displacing container
|
||||
// above them
|
||||
if i != 0 {
|
||||
resize_bottom(&mut stack[i - 1], rect.top);
|
||||
resize_top(&mut stack[i], rect.top);
|
||||
}
|
||||
|
||||
// Containers in stack except last can be resized down displacing container
|
||||
// below them
|
||||
if i != stack.len() - 1 {
|
||||
resize_bottom(&mut stack[i], rect.bottom);
|
||||
resize_top(&mut stack[i + 1], rect.bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_right_vertical_stack_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
match len {
|
||||
// One container can't be resized
|
||||
0 | 1 => (),
|
||||
_ => {
|
||||
let (master, stack) = result.split_at_mut(1);
|
||||
let primary = &mut master[0];
|
||||
|
||||
if let Some(resize) = resize_dimensions[0] {
|
||||
resize_left(primary, resize.left);
|
||||
for s in &mut *stack {
|
||||
resize_right(s, resize.left);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stack on the left
|
||||
for (i, rect) in resize_dimensions[1..].iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
resize_left(primary, rect.right);
|
||||
stack
|
||||
.iter_mut()
|
||||
.for_each(|vertical_element| resize_right(vertical_element, rect.right));
|
||||
|
||||
// Containers in stack except first can be resized up displacing container
|
||||
// above them
|
||||
if i != 0 {
|
||||
resize_bottom(&mut stack[i - 1], rect.top);
|
||||
resize_top(&mut stack[i], rect.top);
|
||||
}
|
||||
|
||||
// Containers in stack except last can be resized down displacing container
|
||||
// below them
|
||||
if i != stack.len() - 1 {
|
||||
resize_bottom(&mut stack[i], rect.bottom);
|
||||
resize_top(&mut stack[i + 1], rect.bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_horizontal_stack_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
match len {
|
||||
0 | 1 => (),
|
||||
_ => {
|
||||
let (primary, rest) = result.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
if let Some(resize_primary) = resize_dimensions[0] {
|
||||
resize_bottom(primary, resize_primary.bottom);
|
||||
|
||||
for horizontal_element in &mut *rest {
|
||||
resize_top(horizontal_element, resize_primary.bottom);
|
||||
}
|
||||
}
|
||||
|
||||
for (i, rect) in resize_dimensions[1..].iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
resize_bottom(primary, rect.top);
|
||||
rest.iter_mut()
|
||||
.for_each(|vertical_element| resize_top(vertical_element, rect.top));
|
||||
|
||||
if i != 0 {
|
||||
resize_right(&mut rest[i - 1], rect.left);
|
||||
resize_left(&mut rest[i], rect.left);
|
||||
}
|
||||
|
||||
if i != rest.len() - 1 {
|
||||
resize_right(&mut rest[i], rect.right);
|
||||
resize_left(&mut rest[i + 1], rect.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
@@ -653,99 +1143,3 @@ fn resize_top(rect: &mut Rect, resize: i32) {
|
||||
fn resize_bottom(rect: &mut Rect, resize: i32) {
|
||||
rect.bottom += resize / 2;
|
||||
}
|
||||
|
||||
fn ultrawide(
|
||||
area: &Rect,
|
||||
len: usize,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
) -> Vec<Rect> {
|
||||
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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let adjustment = calculate_ultrawide_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
layouts
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::Rect;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct CustomLayout(Vec<Column>);
|
||||
|
||||
impl Deref for CustomLayout {
|
||||
@@ -250,7 +250,7 @@ impl CustomLayout {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "column", content = "configuration")]
|
||||
pub enum Column {
|
||||
Primary(Option<ColumnWidth>),
|
||||
@@ -258,18 +258,18 @@ pub enum Column {
|
||||
Tertiary(ColumnSplit),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum ColumnWidth {
|
||||
WidthPercentage(f32),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum ColumnSplit {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum ColumnSplitWithCapacity {
|
||||
Horizontal(usize),
|
||||
Vertical(usize),
|
||||
|
||||
@@ -10,7 +10,6 @@ use strum::EnumString;
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum CycleDirection {
|
||||
Previous,
|
||||
Next,
|
||||
|
||||
@@ -10,9 +10,18 @@ use crate::Rect;
|
||||
use crate::Sizing;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum DefaultLayout {
|
||||
BSP,
|
||||
Columns,
|
||||
@@ -21,6 +30,7 @@ pub enum DefaultLayout {
|
||||
HorizontalStack,
|
||||
UltrawideVerticalStack,
|
||||
Grid,
|
||||
RightMainVerticalStack,
|
||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
||||
}
|
||||
|
||||
@@ -35,7 +45,16 @@ impl DefaultLayout {
|
||||
sizing: Sizing,
|
||||
delta: i32,
|
||||
) -> Option<Rect> {
|
||||
if !matches!(self, Self::BSP) && !matches!(self, Self::UltrawideVerticalStack) {
|
||||
if !matches!(
|
||||
self,
|
||||
Self::BSP
|
||||
| Self::Columns
|
||||
| Self::Rows
|
||||
| Self::VerticalStack
|
||||
| Self::RightMainVerticalStack
|
||||
| Self::HorizontalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
) {
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -137,7 +156,8 @@ impl DefaultLayout {
|
||||
Self::VerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::BSP,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +170,8 @@ impl DefaultLayout {
|
||||
Self::VerticalStack => Self::Rows,
|
||||
Self::Rows => Self::Columns,
|
||||
Self::Columns => Self::Grid,
|
||||
Self::Grid => Self::BSP,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,40 +90,45 @@ impl Direction for DefaultLayout {
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> bool {
|
||||
if count < 2 {
|
||||
return false;
|
||||
}
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Up => match self {
|
||||
Self::BSP => count > 2 && idx != 0 && idx != 1,
|
||||
Self::BSP => idx != 0 && idx != 1,
|
||||
Self::Columns => false,
|
||||
Self::Rows | Self::HorizontalStack => idx != 0,
|
||||
Self::VerticalStack => idx != 0 && idx != 1,
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx > 2,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
OperationDirection::Down => match self {
|
||||
Self::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
|
||||
Self::BSP => idx != count - 1 && idx % 2 != 0,
|
||||
Self::Columns => false,
|
||||
Self::Rows => idx != count - 1,
|
||||
Self::VerticalStack => idx != 0 && idx != count - 1,
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
|
||||
Self::HorizontalStack => idx == 0,
|
||||
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
Self::BSP => count > 1 && idx != 0,
|
||||
Self::BSP => idx != 0,
|
||||
Self::Columns | Self::VerticalStack => idx != 0,
|
||||
Self::RightMainVerticalStack => idx == 0,
|
||||
Self::Rows => false,
|
||||
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => count > 1 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx != 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
Self::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
|
||||
Self::BSP => idx % 2 == 0 && idx != count - 1,
|
||||
Self::Columns => idx != count - 1,
|
||||
Self::Rows => false,
|
||||
Self::VerticalStack => idx == 0,
|
||||
Self::RightMainVerticalStack => idx != 0,
|
||||
Self::HorizontalStack => idx != 0 && idx != count - 1,
|
||||
Self::UltrawideVerticalStack => match count {
|
||||
0 | 1 => false,
|
||||
2 => idx != 0,
|
||||
_ => idx < 2,
|
||||
},
|
||||
@@ -147,7 +152,10 @@ impl Direction for DefaultLayout {
|
||||
}
|
||||
}
|
||||
Self::Columns => unreachable!(),
|
||||
Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx - 1,
|
||||
Self::Rows
|
||||
| Self::VerticalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::RightMainVerticalStack => idx - 1,
|
||||
Self::HorizontalStack => 0,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
}
|
||||
@@ -160,7 +168,11 @@ impl Direction for DefaultLayout {
|
||||
count: Option<usize>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP | Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx + 1,
|
||||
Self::BSP
|
||||
| Self::Rows
|
||||
| Self::VerticalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::RightMainVerticalStack => idx + 1,
|
||||
Self::Columns => unreachable!(),
|
||||
Self::HorizontalStack => 1,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
@@ -184,6 +196,7 @@ impl Direction for DefaultLayout {
|
||||
Self::Columns | Self::HorizontalStack => idx - 1,
|
||||
Self::Rows => unreachable!(),
|
||||
Self::VerticalStack => 0,
|
||||
Self::RightMainVerticalStack => 1,
|
||||
Self::UltrawideVerticalStack => match idx {
|
||||
0 => 1,
|
||||
1 => unreachable!(),
|
||||
@@ -203,6 +216,7 @@ impl Direction for DefaultLayout {
|
||||
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
|
||||
Self::Rows => unreachable!(),
|
||||
Self::VerticalStack => 1,
|
||||
Self::RightMainVerticalStack => 0,
|
||||
Self::UltrawideVerticalStack => match idx {
|
||||
1 => 0,
|
||||
0 => 2,
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Direction;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum Layout {
|
||||
Default(DefaultLayout),
|
||||
Custom(CustomLayout),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self)]
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -14,6 +14,7 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
pub use custom_layout::CustomLayout;
|
||||
@@ -24,6 +25,7 @@ pub use layout::Layout;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
pub mod arrangement;
|
||||
pub mod config_generation;
|
||||
pub mod custom_layout;
|
||||
@@ -43,6 +45,8 @@ pub enum SocketMessage {
|
||||
CycleFocusWindow(CycleDirection),
|
||||
CycleMoveWindow(CycleDirection),
|
||||
StackWindow(OperationDirection),
|
||||
StackAll,
|
||||
UnstackAll,
|
||||
ResizeWindowEdge(OperationDirection, Sizing),
|
||||
ResizeWindowAxis(Axis, Sizing),
|
||||
UnstackWindow,
|
||||
@@ -57,7 +61,9 @@ pub enum SocketMessage {
|
||||
SendContainerToWorkspaceNumber(usize),
|
||||
CycleSendContainerToWorkspace(CycleDirection),
|
||||
SendContainerToMonitorWorkspaceNumber(usize, usize),
|
||||
MoveContainerToMonitorWorkspaceNumber(usize, usize),
|
||||
SendContainerToNamedWorkspace(String),
|
||||
CycleMoveWorkspaceToMonitor(CycleDirection),
|
||||
MoveWorkspaceToMonitorNumber(usize),
|
||||
SwapWorkspacesToMonitorNumber(usize),
|
||||
ForceFocus,
|
||||
@@ -65,6 +71,7 @@ pub enum SocketMessage {
|
||||
Minimize,
|
||||
Promote,
|
||||
PromoteFocus,
|
||||
PromoteWindow(OperationDirection),
|
||||
ToggleFloat,
|
||||
ToggleMonocle,
|
||||
ToggleMaximize,
|
||||
@@ -129,11 +136,31 @@ pub enum SocketMessage {
|
||||
WatchConfiguration(bool),
|
||||
CompleteConfiguration,
|
||||
AltFocusHack(bool),
|
||||
ActiveWindowBorder(bool),
|
||||
ActiveWindowBorderColour(WindowKind, u32, u32, u32),
|
||||
ActiveWindowBorderWidth(i32),
|
||||
ActiveWindowBorderOffset(i32),
|
||||
Animation(bool),
|
||||
AnimationDuration(u64),
|
||||
AnimationFps(u64),
|
||||
AnimationStyle(AnimationStyle),
|
||||
#[serde(alias = "ActiveWindowBorder")]
|
||||
Border(bool),
|
||||
#[serde(alias = "ActiveWindowBorderColour")]
|
||||
BorderColour(WindowKind, u32, u32, u32),
|
||||
#[serde(alias = "ActiveWindowBorderStyle")]
|
||||
BorderStyle(BorderStyle),
|
||||
BorderWidth(i32),
|
||||
BorderOffset(i32),
|
||||
BorderImplementation(BorderImplementation),
|
||||
Transparency(bool),
|
||||
TransparencyAlpha(u8),
|
||||
InvisibleBorders(Rect),
|
||||
StackbarMode(StackbarMode),
|
||||
StackbarLabel(StackbarLabel),
|
||||
StackbarFocusedTextColour(u32, u32, u32),
|
||||
StackbarUnfocusedTextColour(u32, u32, u32),
|
||||
StackbarBackgroundColour(u32, u32, u32),
|
||||
StackbarHeight(i32),
|
||||
StackbarTabWidth(i32),
|
||||
StackbarFontSize(i32),
|
||||
StackbarFontFamily(Option<String>),
|
||||
WorkAreaOffset(Rect),
|
||||
MonitorWorkAreaOffset(usize, Rect),
|
||||
ResizeDelta(i32),
|
||||
@@ -141,6 +168,9 @@ pub enum SocketMessage {
|
||||
InitialNamedWorkspaceRule(ApplicationIdentifier, String, String),
|
||||
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
NamedWorkspaceRule(ApplicationIdentifier, String, String),
|
||||
ClearWorkspaceRules(usize, usize),
|
||||
ClearNamedWorkspaceRules(String),
|
||||
ClearAllWorkspaceRules,
|
||||
FloatRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
IdentifyObjectNameChangeApplication(ApplicationIdentifier, String),
|
||||
@@ -148,7 +178,9 @@ pub enum SocketMessage {
|
||||
IdentifyLayeredApplication(ApplicationIdentifier, String),
|
||||
IdentifyBorderOverflowApplication(ApplicationIdentifier, String),
|
||||
State,
|
||||
GlobalState,
|
||||
VisibleWindows,
|
||||
MonitorInformation,
|
||||
Query(StateQuery),
|
||||
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
|
||||
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
|
||||
@@ -165,6 +197,7 @@ pub enum SocketMessage {
|
||||
SocketSchema,
|
||||
StaticConfigSchema,
|
||||
GenerateStaticConfig,
|
||||
DebugWindow(isize),
|
||||
}
|
||||
|
||||
impl SocketMessage {
|
||||
@@ -181,20 +214,88 @@ impl FromStr for SocketMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum StackbarMode {
|
||||
Always,
|
||||
Never,
|
||||
OnStack,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
pub enum StackbarLabel {
|
||||
#[default]
|
||||
Process,
|
||||
Title,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Display,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
ValueEnum,
|
||||
)]
|
||||
pub enum BorderStyle {
|
||||
#[default]
|
||||
/// Use the system border style
|
||||
System,
|
||||
/// Use the Windows 11-style rounded borders
|
||||
Rounded,
|
||||
/// Use the Windows 10-style square borders
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Display,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
ValueEnum,
|
||||
)]
|
||||
pub enum BorderImplementation {
|
||||
#[default]
|
||||
/// Use the adjustable komorebi border implementation
|
||||
Komorebi,
|
||||
/// Use the thin Windows accent border implementation
|
||||
Windows,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum WindowKind {
|
||||
Single,
|
||||
Stack,
|
||||
Monocle,
|
||||
Unfocused,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum StateQuery {
|
||||
FocusedMonitorIndex,
|
||||
FocusedWorkspaceIndex,
|
||||
@@ -215,7 +316,6 @@ pub enum StateQuery {
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ApplicationIdentifier {
|
||||
#[serde(alias = "exe")]
|
||||
Exe,
|
||||
@@ -230,7 +330,6 @@ pub enum ApplicationIdentifier {
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum FocusFollowsMouseImplementation {
|
||||
/// A custom FFM implementation (slightly more CPU-intensive)
|
||||
Komorebi,
|
||||
@@ -241,7 +340,6 @@ pub enum FocusFollowsMouseImplementation {
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum WindowContainerBehaviour {
|
||||
/// Create a new container for each new window
|
||||
Create,
|
||||
@@ -252,31 +350,30 @@ pub enum WindowContainerBehaviour {
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum MoveBehaviour {
|
||||
/// Swap the window container with the window container at the edge of the adjacent monitor
|
||||
Swap,
|
||||
/// Insert the window container into the focused workspace on the adjacent monitor
|
||||
Insert,
|
||||
/// Do nothing if trying to move a window container in the direction of an adjacent monitor
|
||||
NoOp,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum HidingBehaviour {
|
||||
/// Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
Hide,
|
||||
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
Minimize,
|
||||
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
|
||||
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||
Cloak,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum OperationBehaviour {
|
||||
/// Process komorebic commands on temporarily unmanaged/floated windows
|
||||
Op,
|
||||
@@ -287,7 +384,6 @@ pub enum OperationBehaviour {
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Sizing {
|
||||
Increase,
|
||||
Decrease,
|
||||
|
||||
@@ -13,7 +13,6 @@ use crate::Axis;
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum OperationDirection {
|
||||
Left,
|
||||
Right,
|
||||
|
||||
@@ -84,4 +84,14 @@ impl Rect {
|
||||
bottom: (self.bottom * rect_dpi) / system_dpi,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn rect(&self) -> RECT {
|
||||
RECT {
|
||||
left: self.left,
|
||||
top: self.top,
|
||||
right: self.left + self.right,
|
||||
bottom: self.top + self.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
komorebi-gui/Cargo.toml
Normal file
14
komorebi-gui/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.28-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
egui_extras = { version = "0.28" }
|
||||
eframe = "0.28"
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
serde_json = "1"
|
||||
random_word = { version = "0.4.3", features = ["en"] }
|
||||
windows = { workspace = true }
|
||||
805
komorebi-gui/src/main.rs
Normal file
805
komorebi-gui/src/main.rs
Normal file
@@ -0,0 +1,805 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
use eframe::egui;
|
||||
use eframe::egui::color_picker::Alpha;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use komorebi_client::BorderStyle;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::DefaultLayout;
|
||||
use komorebi_client::GlobalState;
|
||||
use komorebi_client::Layout;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::Rgb;
|
||||
use komorebi_client::RuleDebug;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::StackbarLabel;
|
||||
use komorebi_client::StackbarMode;
|
||||
use komorebi_client::State;
|
||||
use komorebi_client::Window;
|
||||
use komorebi_client::WindowKind;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
|
||||
|
||||
fn main() {
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: ViewportBuilder::default()
|
||||
.with_always_on_top()
|
||||
.with_inner_size([320.0, 500.0]),
|
||||
follow_system_theme: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _ = eframe::run_native(
|
||||
"komorebi-gui",
|
||||
native_options,
|
||||
Box::new(|cc| Ok(Box::new(KomorebiGui::new(cc)))),
|
||||
);
|
||||
}
|
||||
|
||||
struct BorderColours {
|
||||
single: Color32,
|
||||
stack: Color32,
|
||||
monocle: Color32,
|
||||
unfocused: Color32,
|
||||
}
|
||||
|
||||
struct BorderConfig {
|
||||
border_enabled: bool,
|
||||
border_colours: BorderColours,
|
||||
border_style: BorderStyle,
|
||||
border_offset: i32,
|
||||
border_width: i32,
|
||||
}
|
||||
|
||||
struct StackbarConfig {
|
||||
mode: StackbarMode,
|
||||
label: StackbarLabel,
|
||||
height: i32,
|
||||
width: i32,
|
||||
focused_text_colour: Color32,
|
||||
unfocused_text_colour: Color32,
|
||||
background_colour: Color32,
|
||||
}
|
||||
|
||||
struct MonitorConfig {
|
||||
size: Rect,
|
||||
work_area_offset: Rect,
|
||||
workspaces: Vec<WorkspaceConfig>,
|
||||
}
|
||||
|
||||
impl From<&komorebi_client::Monitor> for MonitorConfig {
|
||||
fn from(value: &komorebi_client::Monitor) -> Self {
|
||||
let mut workspaces = vec![];
|
||||
for ws in value.workspaces() {
|
||||
workspaces.push(WorkspaceConfig::from(ws));
|
||||
}
|
||||
|
||||
Self {
|
||||
size: *value.size(),
|
||||
work_area_offset: value.work_area_offset().unwrap_or_default(),
|
||||
workspaces,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WorkspaceConfig {
|
||||
name: String,
|
||||
tile: bool,
|
||||
layout: DefaultLayout,
|
||||
container_padding: i32,
|
||||
workspace_padding: i32,
|
||||
}
|
||||
|
||||
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
|
||||
fn from(value: &komorebi_client::Workspace) -> Self {
|
||||
let layout = match value.layout() {
|
||||
Layout::Default(layout) => *layout,
|
||||
Layout::Custom(_) => DefaultLayout::BSP,
|
||||
};
|
||||
|
||||
let name = value
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| random_word::gen(random_word::Lang::En).to_string());
|
||||
|
||||
Self {
|
||||
layout,
|
||||
name,
|
||||
tile: *value.tile(),
|
||||
workspace_padding: value.workspace_padding().unwrap_or(20),
|
||||
container_padding: value.container_padding().unwrap_or(20),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct KomorebiGui {
|
||||
border_config: BorderConfig,
|
||||
stackbar_config: StackbarConfig,
|
||||
mouse_follows_focus: bool,
|
||||
monitors: Vec<MonitorConfig>,
|
||||
workspace_names: HashMap<usize, Vec<String>>,
|
||||
debug_hwnd: isize,
|
||||
debug_windows: Vec<Window>,
|
||||
debug_rule: Option<RuleDebug>,
|
||||
}
|
||||
|
||||
fn colour32(colour: Option<Colour>) -> Color32 {
|
||||
match colour {
|
||||
Some(Colour::Rgb(rgb)) => Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8),
|
||||
Some(Colour::Hex(hex)) => {
|
||||
let rgb = Rgb::from(hex);
|
||||
Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8)
|
||||
}
|
||||
None => Color32::from_rgb(0, 0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
impl KomorebiGui {
|
||||
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
|
||||
// Restore app state using cc.storage (requires the "persistence" feature).
|
||||
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
|
||||
// for e.g. egui::PaintCallback.
|
||||
let global_state: GlobalState = serde_json::from_str(
|
||||
&komorebi_client::send_query(&SocketMessage::GlobalState).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let state: State =
|
||||
serde_json::from_str(&komorebi_client::send_query(&SocketMessage::State).unwrap())
|
||||
.unwrap();
|
||||
|
||||
let border_colours = BorderColours {
|
||||
single: colour32(global_state.border_colours.single),
|
||||
stack: colour32(global_state.border_colours.stack),
|
||||
monocle: colour32(global_state.border_colours.monocle),
|
||||
unfocused: colour32(global_state.border_colours.unfocused),
|
||||
};
|
||||
|
||||
let border_config = BorderConfig {
|
||||
border_enabled: global_state.border_enabled,
|
||||
border_colours,
|
||||
border_style: global_state.border_style,
|
||||
border_offset: global_state.border_offset,
|
||||
border_width: global_state.border_width,
|
||||
};
|
||||
|
||||
let mut monitors = vec![];
|
||||
for m in state.monitors.elements() {
|
||||
monitors.push(MonitorConfig::from(m));
|
||||
}
|
||||
|
||||
let mut workspace_names = HashMap::new();
|
||||
|
||||
for (monitor_idx, m) in monitors.iter().enumerate() {
|
||||
for ws in &m.workspaces {
|
||||
let names = workspace_names.entry(monitor_idx).or_insert_with(Vec::new);
|
||||
names.push(ws.name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let stackbar_config = StackbarConfig {
|
||||
mode: global_state.stackbar_mode,
|
||||
height: global_state.stackbar_height,
|
||||
width: global_state.stackbar_tab_width,
|
||||
label: global_state.stackbar_label,
|
||||
focused_text_colour: colour32(Some(global_state.stackbar_focused_text_colour)),
|
||||
unfocused_text_colour: colour32(Some(global_state.stackbar_unfocused_text_colour)),
|
||||
background_colour: colour32(Some(global_state.stackbar_tab_background_colour)),
|
||||
};
|
||||
|
||||
let mut debug_windows = vec![];
|
||||
|
||||
unsafe {
|
||||
EnumWindows(
|
||||
Some(enum_window),
|
||||
windows::Win32::Foundation::LPARAM(&mut debug_windows as *mut Vec<Window> as isize),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
Self {
|
||||
border_config,
|
||||
mouse_follows_focus: state.mouse_follows_focus,
|
||||
monitors,
|
||||
workspace_names,
|
||||
debug_hwnd: 0,
|
||||
debug_windows,
|
||||
stackbar_config,
|
||||
debug_rule: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "system" fn enum_window(
|
||||
hwnd: windows::Win32::Foundation::HWND,
|
||||
lparam: windows::Win32::Foundation::LPARAM,
|
||||
) -> windows::Win32::Foundation::BOOL {
|
||||
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
|
||||
let window = Window::from(hwnd.0);
|
||||
|
||||
if window.is_window()
|
||||
&& !window.is_miminized()
|
||||
&& window.is_visible()
|
||||
&& window.title().is_ok()
|
||||
&& window.exe().is_ok()
|
||||
{
|
||||
windows.push(window);
|
||||
}
|
||||
|
||||
true.into()
|
||||
}
|
||||
|
||||
fn json_view_ui(ui: &mut egui::Ui, code: &str) {
|
||||
let language = "json";
|
||||
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
|
||||
}
|
||||
|
||||
impl eframe::App for KomorebiGui {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ctx.set_pixels_per_point(2.0);
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.set_width(ctx.screen_rect().width());
|
||||
ui.collapsing("Debugging", |ui| {
|
||||
ui.collapsing("Window Rules", |ui| {
|
||||
let window = Window::from(self.debug_hwnd);
|
||||
|
||||
let label = if let (Ok(title), Ok(exe)) = (window.title(), window.exe()) {
|
||||
format!("{title} ({exe})")
|
||||
} else {
|
||||
String::from("Select a Window")
|
||||
};
|
||||
|
||||
if ui.button("Refresh Windows").clicked() {
|
||||
let mut debug_windows = vec![];
|
||||
|
||||
unsafe {
|
||||
EnumWindows(
|
||||
Some(enum_window),
|
||||
windows::Win32::Foundation::LPARAM(
|
||||
&mut debug_windows as *mut Vec<Window> as isize,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
self.debug_windows = debug_windows;
|
||||
}
|
||||
|
||||
egui::ComboBox::from_label("Select a Window")
|
||||
.selected_text(label)
|
||||
.show_ui(ui, |ui| {
|
||||
for w in &self.debug_windows {
|
||||
if ui
|
||||
.selectable_value(
|
||||
&mut self.debug_hwnd,
|
||||
w.hwnd,
|
||||
format!(
|
||||
"{} ({})",
|
||||
w.title().unwrap(),
|
||||
w.exe().unwrap()
|
||||
),
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
let debug_rule: RuleDebug = serde_json::from_str(
|
||||
&komorebi_client::send_query(
|
||||
&SocketMessage::DebugWindow(self.debug_hwnd),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.debug_rule = Some(debug_rule)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(debug_rule) = &self.debug_rule {
|
||||
json_view_ui(ui, &serde_json::to_string_pretty(debug_rule).unwrap())
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.collapsing("Mouse", |ui| {
|
||||
if ui
|
||||
.toggle_value(&mut self.mouse_follows_focus, "Mouse Follows Focus")
|
||||
.changed()
|
||||
{
|
||||
komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
self.mouse_follows_focus,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Border", |ui| {
|
||||
if ui
|
||||
.toggle_value(&mut self.border_config.border_enabled, "Border")
|
||||
.changed()
|
||||
{
|
||||
komorebi_client::send_message(&SocketMessage::Border(
|
||||
self.border_config.border_enabled,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
ui.collapsing("Colours", |ui| {
|
||||
ui.collapsing("Single", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.border_config.border_colours.single,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(&SocketMessage::BorderColour(
|
||||
WindowKind::Single,
|
||||
self.border_config.border_colours.single.r() as u32,
|
||||
self.border_config.border_colours.single.g() as u32,
|
||||
self.border_config.border_colours.single.b() as u32,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Stack", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.border_config.border_colours.stack,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(&SocketMessage::BorderColour(
|
||||
WindowKind::Stack,
|
||||
self.border_config.border_colours.stack.r() as u32,
|
||||
self.border_config.border_colours.stack.g() as u32,
|
||||
self.border_config.border_colours.stack.b() as u32,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Monocle", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.border_config.border_colours.monocle,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(&SocketMessage::BorderColour(
|
||||
WindowKind::Monocle,
|
||||
self.border_config.border_colours.monocle.r() as u32,
|
||||
self.border_config.border_colours.monocle.g() as u32,
|
||||
self.border_config.border_colours.monocle.b() as u32,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Unfocused", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.border_config.border_colours.unfocused,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(&SocketMessage::BorderColour(
|
||||
WindowKind::Unfocused,
|
||||
self.border_config.border_colours.unfocused.r() as u32,
|
||||
self.border_config.border_colours.unfocused.g() as u32,
|
||||
self.border_config.border_colours.unfocused.b() as u32,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
ui.collapsing("Style", |ui| {
|
||||
for option in [
|
||||
BorderStyle::System,
|
||||
BorderStyle::Rounded,
|
||||
BorderStyle::Square,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
self.border_config.border_style == option,
|
||||
option.to_string(),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
self.border_config.border_style = option;
|
||||
komorebi_client::send_message(&SocketMessage::BorderStyle(
|
||||
self.border_config.border_style,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
komorebi_client::send_message(&SocketMessage::Retile).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Width", |ui| {
|
||||
if ui
|
||||
.add(egui::Slider::new(
|
||||
&mut self.border_config.border_width,
|
||||
-50..=50,
|
||||
))
|
||||
.changed()
|
||||
{
|
||||
komorebi_client::send_message(&SocketMessage::BorderWidth(
|
||||
self.border_config.border_width,
|
||||
))
|
||||
.unwrap();
|
||||
};
|
||||
});
|
||||
|
||||
ui.collapsing("Offset", |ui| {
|
||||
if ui
|
||||
.add(egui::Slider::new(
|
||||
&mut self.border_config.border_offset,
|
||||
-50..=50,
|
||||
))
|
||||
.changed()
|
||||
{
|
||||
komorebi_client::send_message(&SocketMessage::BorderOffset(
|
||||
self.border_config.border_offset,
|
||||
))
|
||||
.unwrap();
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
ui.collapsing("Stackbar", |ui| {
|
||||
for option in [
|
||||
StackbarMode::Never,
|
||||
StackbarMode::OnStack,
|
||||
StackbarMode::Always,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
self.stackbar_config.mode == option,
|
||||
option.to_string(),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
self.stackbar_config.mode = option;
|
||||
komorebi_client::send_message(&SocketMessage::StackbarMode(
|
||||
self.stackbar_config.mode,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
ui.collapsing("Label", |ui| {
|
||||
for option in [StackbarLabel::Process, StackbarLabel::Title] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
self.stackbar_config.label == option,
|
||||
option.to_string(),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
self.stackbar_config.label = option;
|
||||
komorebi_client::send_message(&SocketMessage::StackbarLabel(
|
||||
self.stackbar_config.label,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Colours", |ui| {
|
||||
ui.collapsing("Focused Text", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.stackbar_config.focused_text_colour,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::StackbarFocusedTextColour(
|
||||
self.stackbar_config.focused_text_colour.r() as u32,
|
||||
self.stackbar_config.focused_text_colour.g() as u32,
|
||||
self.stackbar_config.focused_text_colour.b() as u32,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Unfocused Text", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.stackbar_config.unfocused_text_colour,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::StackbarUnfocusedTextColour(
|
||||
self.stackbar_config.unfocused_text_colour.r() as u32,
|
||||
self.stackbar_config.unfocused_text_colour.g() as u32,
|
||||
self.stackbar_config.unfocused_text_colour.b() as u32,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Background", |ui| {
|
||||
if egui::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.stackbar_config.background_colour,
|
||||
Alpha::Opaque,
|
||||
) {
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::StackbarBackgroundColour(
|
||||
self.stackbar_config.background_colour.r() as u32,
|
||||
self.stackbar_config.background_colour.g() as u32,
|
||||
self.stackbar_config.background_colour.b() as u32,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
ui.collapsing("Width", |ui| {
|
||||
if ui
|
||||
.add(egui::Slider::new(&mut self.stackbar_config.width, 0..=500))
|
||||
.drag_stopped()
|
||||
{
|
||||
komorebi_client::send_message(&SocketMessage::StackbarTabWidth(
|
||||
self.stackbar_config.width,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
|
||||
};
|
||||
});
|
||||
|
||||
ui.collapsing("Height", |ui| {
|
||||
if ui
|
||||
.add(egui::Slider::new(&mut self.stackbar_config.height, 0..=100))
|
||||
.drag_stopped()
|
||||
{
|
||||
komorebi_client::send_message(&SocketMessage::StackbarHeight(
|
||||
self.stackbar_config.height,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
komorebi_client::send_message(&SocketMessage::Retile).unwrap()
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
for (monitor_idx, monitor) in self.monitors.iter_mut().enumerate() {
|
||||
ui.collapsing(
|
||||
format!(
|
||||
"Monitor {monitor_idx} ({}x{})",
|
||||
monitor.size.right, monitor.size.bottom
|
||||
),
|
||||
|ui| {
|
||||
ui.collapsing("Work Area Offset", |ui| {
|
||||
if ui
|
||||
.add(
|
||||
egui::Slider::new(
|
||||
&mut monitor.work_area_offset.left,
|
||||
0..=500,
|
||||
)
|
||||
.text("Left"),
|
||||
)
|
||||
.drag_stopped()
|
||||
{
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
monitor.work_area_offset,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
if ui
|
||||
.add(
|
||||
egui::Slider::new(
|
||||
&mut monitor.work_area_offset.top,
|
||||
0..=500,
|
||||
)
|
||||
.text("Top"),
|
||||
)
|
||||
.drag_stopped()
|
||||
{
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
monitor.work_area_offset,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
if ui
|
||||
.add(
|
||||
egui::Slider::new(
|
||||
&mut monitor.work_area_offset.right,
|
||||
0..=500,
|
||||
)
|
||||
.text("Right"),
|
||||
)
|
||||
.drag_stopped()
|
||||
{
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
monitor.work_area_offset,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
if ui
|
||||
.add(
|
||||
egui::Slider::new(
|
||||
&mut monitor.work_area_offset.bottom,
|
||||
0..=500,
|
||||
)
|
||||
.text("Bottom"),
|
||||
)
|
||||
.drag_stopped()
|
||||
{
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
monitor.work_area_offset,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
});
|
||||
|
||||
ui.collapsing("Workspaces", |ui| {
|
||||
for (workspace_idx, workspace) in
|
||||
monitor.workspaces.iter_mut().enumerate()
|
||||
{
|
||||
ui.collapsing(
|
||||
format!("Workspace {workspace_idx} ({})", workspace.name),
|
||||
|ui| {
|
||||
if ui.button("Focus").clicked() {
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::MouseFollowsFocus(false),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::FocusMonitorWorkspaceNumber(
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::MouseFollowsFocus(
|
||||
self.mouse_follows_focus,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if ui
|
||||
.toggle_value(&mut workspace.tile, "Tiling")
|
||||
.changed()
|
||||
{
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::WorkspaceTiling(
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
workspace.tile,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
ui.collapsing("Name", |ui| {
|
||||
let monitor_workspaces = self
|
||||
.workspace_names
|
||||
.get_mut(&monitor_idx)
|
||||
.unwrap();
|
||||
let workspace_name =
|
||||
&mut monitor_workspaces[workspace_idx];
|
||||
if ui
|
||||
.text_edit_singleline(workspace_name)
|
||||
.lost_focus()
|
||||
{
|
||||
workspace.name.clone_from(workspace_name);
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::WorkspaceName(
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
workspace.name.clone(),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Layout", |ui| {
|
||||
for option in [
|
||||
DefaultLayout::BSP,
|
||||
DefaultLayout::Columns,
|
||||
DefaultLayout::Rows,
|
||||
DefaultLayout::VerticalStack,
|
||||
DefaultLayout::HorizontalStack,
|
||||
DefaultLayout::UltrawideVerticalStack,
|
||||
DefaultLayout::Grid,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
workspace.layout == option,
|
||||
option.to_string(),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
workspace.layout = option;
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::WorkspaceLayout(
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
workspace.layout,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.collapsing("Container Padding", |ui| {
|
||||
if ui
|
||||
.add(egui::Slider::new(
|
||||
&mut workspace.container_padding,
|
||||
0..=100,
|
||||
))
|
||||
.drag_stopped()
|
||||
{
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::ContainerPadding(
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
workspace.container_padding,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
});
|
||||
|
||||
ui.collapsing("Workspace Padding", |ui| {
|
||||
if ui
|
||||
.add(egui::Slider::new(
|
||||
&mut workspace.workspace_padding,
|
||||
0..=100,
|
||||
))
|
||||
.drag_stopped()
|
||||
{
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::WorkspacePadding(
|
||||
monitor_idx,
|
||||
workspace_idx,
|
||||
workspace.workspace_padding,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
#SingleInstance Force
|
||||
|
||||
; Load library
|
||||
#Include komorebic.lib.ahk
|
||||
|
||||
; Focus windows
|
||||
!h::Focus("left")
|
||||
!j::Focus("down")
|
||||
!k::Focus("up")
|
||||
!l::Focus("right")
|
||||
!+[::CycleFocus("previous")
|
||||
!+]::CycleFocus("next")
|
||||
|
||||
; Move windows
|
||||
!+h::Move("left")
|
||||
!+j::Move("down")
|
||||
!+k::Move("up")
|
||||
!+l::Move("right")
|
||||
!+Enter::Promote()
|
||||
|
||||
; Stack windows
|
||||
!Left::Stack("left")
|
||||
!Right::Stack("right")
|
||||
!Up::Stack("up")
|
||||
!Down::Stack("down")
|
||||
!;::Unstack()
|
||||
![::CycleStack("previous")
|
||||
!]::CycleStack("next")
|
||||
|
||||
; Resize
|
||||
!=::ResizeAxis("horizontal", "increase")
|
||||
!-::ResizeAxis("horizontal", "decrease")
|
||||
!+=::ResizeAxis("vertical", "increase")
|
||||
!+-::ResizeAxis("vertical", "decrease")
|
||||
|
||||
; Manipulate windows
|
||||
!t::ToggleFloat()
|
||||
!+f::ToggleMonocle()
|
||||
|
||||
; Window manager options
|
||||
!+r::Retile()
|
||||
!p::TogglePause()
|
||||
|
||||
; Layouts
|
||||
!x::FlipLayout("horizontal")
|
||||
!y::FlipLayout("vertical")
|
||||
|
||||
; Workspaces
|
||||
!1::FocusWorkspace(0)
|
||||
!2::FocusWorkspace(1)
|
||||
!3::FocusWorkspace(2)
|
||||
|
||||
; Move windows across workspaces
|
||||
!+1::MoveToWorkspace(0)
|
||||
!+2::MoveToWorkspace(1)
|
||||
!+3::MoveToWorkspace(2)
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.25-dev.0"
|
||||
version = "0.1.28-dev.0"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
@@ -13,40 +13,45 @@ edition = "2021"
|
||||
[dependencies]
|
||||
komorebi-core = { path = "../komorebi-core" }
|
||||
|
||||
bitflags = "2"
|
||||
bitflags = { version = "2", features = ["serde"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
color-eyre = { workspace = true }
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
ctrlc = "3"
|
||||
ctrlc = { version = "3", features = ["termination"] }
|
||||
dirs = { workspace = true }
|
||||
getset = "0.1"
|
||||
hex_color = { version = "3", features = ["serde"] }
|
||||
hotwatch = "0.5"
|
||||
lazy_static = "1"
|
||||
miow = "0.5"
|
||||
miow = "0.6"
|
||||
nanoid = "0.4"
|
||||
net2 = "0.2"
|
||||
os_info = "3.8"
|
||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||
parking_lot = "0.12"
|
||||
paste = "1"
|
||||
regex = "1"
|
||||
schemars = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
sysinfo = "0.30"
|
||||
sysinfo = { workspace = true }
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uds_windows = "1"
|
||||
which = "6"
|
||||
widestring = "1"
|
||||
win32-display-data = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.52"
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
[features]
|
||||
deadlock_detection = []
|
||||
deadlock_detection = ["parking_lot/deadlock_detection"]
|
||||
|
||||
3
komorebi/build.rs
Normal file
3
komorebi/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
shadow_rs::new().unwrap();
|
||||
}
|
||||
503
komorebi/src/animation.rs
Normal file
503
komorebi/src/animation.rs
Normal file
@@ -0,0 +1,503 @@
|
||||
use color_eyre::Result;
|
||||
use komorebi_core::AnimationStyle;
|
||||
use komorebi_core::Rect;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::f64::consts::PI;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_MANAGER;
|
||||
use crate::ANIMATION_STYLE;
|
||||
|
||||
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(60);
|
||||
|
||||
pub trait Ease {
|
||||
fn evaluate(t: f64) -> f64;
|
||||
}
|
||||
|
||||
pub struct Linear;
|
||||
|
||||
impl Ease for Linear {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInSine;
|
||||
|
||||
impl Ease for EaseInSine {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - f64::cos((t * PI) / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutSine;
|
||||
|
||||
impl Ease for EaseOutSine {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
f64::sin((t * PI) / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutSine;
|
||||
|
||||
impl Ease for EaseInOutSine {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
-(f64::cos(PI * t) - 1.0) / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInQuad;
|
||||
|
||||
impl Ease for EaseInQuad {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutQuad;
|
||||
|
||||
impl Ease for EaseOutQuad {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
(1.0 - t).mul_add(-1.0 - t, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutQuad;
|
||||
|
||||
impl Ease for EaseInOutQuad {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
2.0 * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(2) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInCubic;
|
||||
|
||||
impl Ease for EaseInCubic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutCubic;
|
||||
|
||||
impl Ease for EaseOutCubic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - (1.0 - t).powi(3)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutCubic;
|
||||
|
||||
impl Ease for EaseInOutCubic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
4.0 * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(3) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInQuart;
|
||||
|
||||
impl Ease for EaseInQuart {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t * t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutQuart;
|
||||
|
||||
impl Ease for EaseOutQuart {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - (1.0 - t).powi(4)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutQuart;
|
||||
|
||||
impl Ease for EaseInOutQuart {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
8.0 * t * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(4) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInQuint;
|
||||
|
||||
impl Ease for EaseInQuint {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t * t * t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutQuint;
|
||||
|
||||
impl Ease for EaseOutQuint {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - (1.0 - t).powi(5)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutQuint;
|
||||
|
||||
impl Ease for EaseInOutQuint {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
16.0 * t * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(5) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInExpo;
|
||||
|
||||
impl Ease for EaseInExpo {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
10.0f64.mul_add(t, -10.0).exp2()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutExpo;
|
||||
|
||||
impl Ease for EaseOutExpo {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON {
|
||||
return t;
|
||||
}
|
||||
|
||||
1.0 - (-10.0 * t).exp2()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutExpo;
|
||||
|
||||
impl Ease for EaseInOutExpo {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t == 0.0 || (t - 1.0).abs() < f64::EPSILON {
|
||||
return t;
|
||||
}
|
||||
|
||||
if t < 0.5 {
|
||||
20.0f64.mul_add(t, -10.0).exp2() / 2.0
|
||||
} else {
|
||||
(2.0 - (-20.0f64).mul_add(t, 10.0).exp2()) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInCirc;
|
||||
|
||||
impl Ease for EaseInCirc {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - f64::sqrt(t.mul_add(-t, 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutCirc;
|
||||
|
||||
impl Ease for EaseOutCirc {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
f64::sqrt((t - 1.0).mul_add(-(t - 1.0), 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutCirc;
|
||||
|
||||
impl Ease for EaseInOutCirc {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
(1.0 - f64::sqrt((2.0 * t).mul_add(-(2.0 * t), 1.0))) / 2.0
|
||||
} else {
|
||||
(f64::sqrt(
|
||||
(-2.0f64)
|
||||
.mul_add(t, 2.0)
|
||||
.mul_add(-(-2.0f64).mul_add(t, 2.0), 1.0),
|
||||
) + 1.0)
|
||||
/ 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInBack;
|
||||
|
||||
impl Ease for EaseInBack {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let c1 = 1.70158;
|
||||
let c3 = c1 + 1.0;
|
||||
|
||||
(c3 * t * t).mul_add(t, -c1 * t * t)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutBack;
|
||||
|
||||
impl Ease for EaseOutBack {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let c1: f64 = 1.70158;
|
||||
let c3: f64 = c1 + 1.0;
|
||||
|
||||
c1.mul_add((t - 1.0).powi(2), c3.mul_add((t - 1.0).powi(3), 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutBack;
|
||||
|
||||
impl Ease for EaseInOutBack {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let c1: f64 = 1.70158;
|
||||
let c2: f64 = c1 * 1.525;
|
||||
|
||||
if t < 0.5 {
|
||||
((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0).mul_add(t, -c2)) / 2.0
|
||||
} else {
|
||||
((2.0f64.mul_add(t, -2.0))
|
||||
.powi(2)
|
||||
.mul_add((c2 + 1.0).mul_add(t.mul_add(2.0, -2.0), c2), 2.0))
|
||||
/ 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInElastic;
|
||||
|
||||
impl Ease for EaseInElastic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
let c4 = (2.0 * PI) / 3.0;
|
||||
|
||||
-(10.0f64.mul_add(t, -10.0).exp2()) * f64::sin(t.mul_add(10.0, -10.75) * c4)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutElastic;
|
||||
|
||||
impl Ease for EaseOutElastic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
let c4 = (2.0 * PI) / 3.0;
|
||||
|
||||
(-10.0 * t)
|
||||
.exp2()
|
||||
.mul_add(f64::sin(t.mul_add(10.0, -0.75) * c4), 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutElastic;
|
||||
|
||||
impl Ease for EaseInOutElastic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
let c5 = (2.0 * PI) / 4.5;
|
||||
|
||||
if t < 0.5 {
|
||||
-(20.0f64.mul_add(t, -10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0
|
||||
} else {
|
||||
((-20.0f64).mul_add(t, 10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0
|
||||
+ 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInBounce;
|
||||
|
||||
impl Ease for EaseInBounce {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - EaseOutBounce::evaluate(1.0 - t)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutBounce;
|
||||
|
||||
impl Ease for EaseOutBounce {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let mut time = t;
|
||||
let n1 = 7.5625;
|
||||
let d1 = 2.75;
|
||||
|
||||
if t < 1.0 / d1 {
|
||||
n1 * time * time
|
||||
} else if time < 2.0 / d1 {
|
||||
time -= 1.5 / d1;
|
||||
(n1 * time).mul_add(time, 0.75)
|
||||
} else if time < 2.5 / d1 {
|
||||
time -= 2.25 / d1;
|
||||
(n1 * time).mul_add(time, 0.9375)
|
||||
} else {
|
||||
time -= 2.625 / d1;
|
||||
(n1 * time).mul_add(time, 0.984_375)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutBounce;
|
||||
|
||||
impl Ease for EaseInOutBounce {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
(1.0 - EaseOutBounce::evaluate(2.0f64.mul_add(-t, 1.0))) / 2.0
|
||||
} else {
|
||||
(1.0 + EaseOutBounce::evaluate(2.0f64.mul_add(t, -1.0))) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
fn apply_ease_func(t: f64) -> f64 {
|
||||
let style = *ANIMATION_STYLE.lock();
|
||||
|
||||
match style {
|
||||
AnimationStyle::Linear => Linear::evaluate(t),
|
||||
AnimationStyle::EaseInSine => EaseInSine::evaluate(t),
|
||||
AnimationStyle::EaseOutSine => EaseOutSine::evaluate(t),
|
||||
AnimationStyle::EaseInOutSine => EaseInOutSine::evaluate(t),
|
||||
AnimationStyle::EaseInQuad => EaseInQuad::evaluate(t),
|
||||
AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
|
||||
AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t),
|
||||
AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
|
||||
AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t),
|
||||
AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuart => EaseInOutQuart::evaluate(t),
|
||||
AnimationStyle::EaseInQuint => EaseInQuint::evaluate(t),
|
||||
AnimationStyle::EaseOutQuint => EaseOutQuint::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuint => EaseInOutQuint::evaluate(t),
|
||||
AnimationStyle::EaseInExpo => EaseInExpo::evaluate(t),
|
||||
AnimationStyle::EaseOutExpo => EaseOutExpo::evaluate(t),
|
||||
AnimationStyle::EaseInOutExpo => EaseInOutExpo::evaluate(t),
|
||||
AnimationStyle::EaseInCirc => EaseInCirc::evaluate(t),
|
||||
AnimationStyle::EaseOutCirc => EaseOutCirc::evaluate(t),
|
||||
AnimationStyle::EaseInOutCirc => EaseInOutCirc::evaluate(t),
|
||||
AnimationStyle::EaseInBack => EaseInBack::evaluate(t),
|
||||
AnimationStyle::EaseOutBack => EaseOutBack::evaluate(t),
|
||||
AnimationStyle::EaseInOutBack => EaseInOutBack::evaluate(t),
|
||||
AnimationStyle::EaseInElastic => EaseInElastic::evaluate(t),
|
||||
AnimationStyle::EaseOutElastic => EaseOutElastic::evaluate(t),
|
||||
AnimationStyle::EaseInOutElastic => EaseInOutElastic::evaluate(t),
|
||||
AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t),
|
||||
AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
|
||||
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Animation {
|
||||
pub hwnd: isize,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(hwnd: isize) -> Self {
|
||||
Self { hwnd }
|
||||
}
|
||||
pub fn cancel(&mut self) {
|
||||
if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().cancel(self.hwnd);
|
||||
let max_duration = Duration::from_secs(1);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(
|
||||
ANIMATION_DURATION.load(Ordering::SeqCst) / 2,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn lerp(start: i32, end: i32, t: f64) -> i32 {
|
||||
let time = apply_ease_func(t);
|
||||
f64::from(end - start)
|
||||
.mul_add(time, f64::from(start))
|
||||
.round() as i32
|
||||
}
|
||||
|
||||
pub fn lerp_rect(start_rect: &Rect, end_rect: &Rect, t: f64) -> Rect {
|
||||
Rect {
|
||||
left: Self::lerp(start_rect.left, end_rect.left, t),
|
||||
top: Self::lerp(start_rect.top, end_rect.top, t),
|
||||
right: Self::lerp(start_rect.right, end_rect.right, t),
|
||||
bottom: Self::lerp(start_rect.bottom, end_rect.bottom, t),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
&mut self,
|
||||
duration: Duration,
|
||||
mut render_callback: impl FnMut(f64) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
if ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
self.cancel();
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().start(self.hwnd);
|
||||
|
||||
let target_frame_time = Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
|
||||
let mut progress = 0.0;
|
||||
let animation_start = Instant::now();
|
||||
|
||||
// start animation
|
||||
while progress < 1.0 {
|
||||
// check if animation is cancelled
|
||||
if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) {
|
||||
// cancel animation
|
||||
// set all flags
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame_start = Instant::now();
|
||||
// calculate progress
|
||||
progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
|
||||
render_callback(progress).ok();
|
||||
|
||||
// sleep until next frame
|
||||
if frame_start.elapsed() < target_frame_time {
|
||||
std::thread::sleep(target_frame_time - frame_start.elapsed());
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
|
||||
// limit progress to 1.0 if animation took longer
|
||||
if progress > 1.0 {
|
||||
progress = 1.0;
|
||||
}
|
||||
|
||||
// process animation for 1.0 to set target position
|
||||
render_callback(progress)
|
||||
}
|
||||
}
|
||||
79
komorebi/src/animation_manager.rs
Normal file
79
komorebi/src/animation_manager.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub static ANIMATIONS_IN_PROGRESS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct AnimationState {
|
||||
pub in_progress: bool,
|
||||
pub is_cancelled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AnimationManager {
|
||||
animations: HashMap<isize, AnimationState>,
|
||||
}
|
||||
|
||||
impl Default for AnimationManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
animations: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self, hwnd: isize) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(&hwnd) {
|
||||
animation_state.is_cancelled
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_progress(&self, hwnd: isize) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(&hwnd) {
|
||||
animation_state.in_progress
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.is_cancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, hwnd: isize) {
|
||||
if let Entry::Vacant(e) = self.animations.entry(hwnd) {
|
||||
e.insert(AnimationState {
|
||||
in_progress: true,
|
||||
is_cancelled: false,
|
||||
});
|
||||
|
||||
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = false;
|
||||
animation_state.is_cancelled = false;
|
||||
|
||||
self.animations.remove(&hwnd);
|
||||
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use color_eyre::Result;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::FindWindowW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
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::WNDCLASSW;
|
||||
|
||||
use crate::window::Window;
|
||||
use crate::windows_callbacks;
|
||||
use crate::WindowsApi;
|
||||
use crate::BORDER_HWND;
|
||||
use crate::BORDER_OFFSET;
|
||||
use crate::BORDER_RECT;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::TRANSPARENCY_COLOUR;
|
||||
|
||||
#[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: Vec<u16> = format!("{name}\0").encode_utf16().collect();
|
||||
let instance = WindowsApi::module_handle_w()?;
|
||||
let class_name = PCWSTR(name.as_ptr());
|
||||
let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
|
||||
let window_class = WNDCLASSW {
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(windows_callbacks::border_window),
|
||||
hbrBackground: brush,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _atom = WindowsApi::register_class_w(&window_class)?;
|
||||
|
||||
let name_cl = name.clone();
|
||||
std::thread::spawn(move || -> Result<()> {
|
||||
let hwnd = WindowsApi::create_border_window(PCWSTR(name_cl.as_ptr()), instance)?;
|
||||
let border = Self::from(hwnd);
|
||||
|
||||
let mut message = MSG::default();
|
||||
|
||||
unsafe {
|
||||
while GetMessageW(&mut message, border.hwnd(), 0, 0).into() {
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut hwnd = HWND(0);
|
||||
while hwnd == HWND(0) {
|
||||
hwnd = unsafe { FindWindowW(PCWSTR(name.as_ptr()), PCWSTR::null()) };
|
||||
}
|
||||
|
||||
BORDER_HWND.store(hwnd.0, Ordering::SeqCst);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide(self) -> Result<()> {
|
||||
if self.hwnd == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
WindowsApi::hide_border_window(self.hwnd())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_position(self, window: Window, activate: bool) -> Result<()> {
|
||||
if self.hwnd == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
if !WindowsApi::is_window(self.hwnd()) {
|
||||
Self::create("komorebi-border-window")?;
|
||||
}
|
||||
|
||||
let mut rect = WindowsApi::window_rect(window.hwnd())?;
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
|
||||
|
||||
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
rect.add_margin(border_width);
|
||||
|
||||
*BORDER_RECT.lock() = rect;
|
||||
|
||||
WindowsApi::position_border_window(self.hwnd(), &rect, activate)
|
||||
}
|
||||
}
|
||||
}
|
||||
225
komorebi/src/border_manager/border.rs
Normal file
225
komorebi/src/border_manager/border.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::FOCUS_STATE;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
use komorebi_core::BorderStyle;
|
||||
use komorebi_core::Rect;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Gdi::BeginPaint;
|
||||
use windows::Win32::Graphics::Gdi::CreatePen;
|
||||
use windows::Win32::Graphics::Gdi::DeleteObject;
|
||||
use windows::Win32::Graphics::Gdi::EndPaint;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::RoundRect;
|
||||
use windows::Win32::Graphics::Gdi::SelectObject;
|
||||
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
|
||||
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
|
||||
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||
|
||||
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
|
||||
if class.starts_with("komoborder") {
|
||||
hwnds.push(hwnd.0);
|
||||
}
|
||||
}
|
||||
|
||||
true.into()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Border {
|
||||
pub hwnd: isize,
|
||||
}
|
||||
|
||||
impl From<isize> for Border {
|
||||
fn from(value: isize) -> Self {
|
||||
Self { hwnd: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Border {
|
||||
pub const fn hwnd(&self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn create(id: &str) -> color_eyre::Result<Self> {
|
||||
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
|
||||
let class_name = PCWSTR(name.as_ptr());
|
||||
|
||||
let h_module = WindowsApi::module_handle_w()?;
|
||||
|
||||
let window_class = WNDCLASSW {
|
||||
hInstance: h_module.into(),
|
||||
lpszClassName: class_name,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(Self::callback),
|
||||
hbrBackground: WindowsApi::create_solid_brush(0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _ = WindowsApi::register_class_w(&window_class);
|
||||
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
|
||||
std::thread::spawn(move || -> color_eyre::Result<()> {
|
||||
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), h_module)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
|
||||
let mut msg: MSG = MSG::default();
|
||||
|
||||
loop {
|
||||
unsafe {
|
||||
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
|
||||
tracing::debug!("border window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
hwnd: hwnd_receiver.recv()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
WindowsApi::close_window(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn update(&self, rect: &Rect, mut should_invalidate: bool) -> color_eyre::Result<()> {
|
||||
// Make adjustments to the border
|
||||
let mut rect = *rect;
|
||||
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst));
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
|
||||
|
||||
// Update the position of the border if required
|
||||
if !WindowsApi::window_rect(self.hwnd())?.eq(&rect) {
|
||||
WindowsApi::set_border_pos(self.hwnd(), &rect, HWND((Z_ORDER.load()).into()))?;
|
||||
should_invalidate = true;
|
||||
}
|
||||
|
||||
// Invalidate the rect to trigger the callback to update colours etc.
|
||||
if should_invalidate {
|
||||
self.invalidate();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn invalidate(&self) {
|
||||
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
|
||||
}
|
||||
|
||||
pub extern "system" fn callback(
|
||||
window: HWND,
|
||||
message: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_PAINT => {
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
|
||||
// With the rect that we set in Self::update
|
||||
match WindowsApi::window_rect(window) {
|
||||
Ok(rect) => {
|
||||
// Grab the focus kind for this border
|
||||
let window_kind = {
|
||||
FOCUS_STATE
|
||||
.lock()
|
||||
.get(&window.0)
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused)
|
||||
};
|
||||
|
||||
// Set up the brush to draw the border
|
||||
let hpen = CreatePen(
|
||||
PS_SOLID | PS_INSIDEFRAME,
|
||||
BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
COLORREF(window_kind_colour(window_kind)),
|
||||
);
|
||||
|
||||
let hbrush = WindowsApi::create_solid_brush(0);
|
||||
|
||||
// Draw the border
|
||||
SelectObject(hdc, hpen);
|
||||
SelectObject(hdc, hbrush);
|
||||
// TODO(raggi): this is approximately the correct curvature for
|
||||
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
|
||||
// often the bottom right has a different shape. Furthermore if
|
||||
// the window was made with DWMWCP_ROUNDSMALL then this is the
|
||||
// wrong size. In the future we should read the DWM properties
|
||||
// of windows and attempt to match appropriately.
|
||||
match STYLE.load() {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
} else {
|
||||
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => {
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
DeleteObject(hpen);
|
||||
DeleteObject(hbrush);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("could not get border rect: {}", error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
EndPaint(window, &ps);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(window, message, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
481
komorebi/src/border_manager/mod.rs
Normal file
481
komorebi/src/border_manager/mod.rs
Normal file
@@ -0,0 +1,481 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod border;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::BorderImplementation;
|
||||
use komorebi_core::BorderStyle;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::Colour;
|
||||
use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
use border::Border;
|
||||
use komorebi_core::WindowKind;
|
||||
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
|
||||
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
|
||||
|
||||
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
|
||||
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
|
||||
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
|
||||
AtomicCell::new(BorderImplementation::Windows);
|
||||
pub static ref FOCUSED: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245))));
|
||||
pub static ref UNFOCUSED: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(128, 128, 128))));
|
||||
pub static ref MONOCLE: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(255, 51, 153))));
|
||||
pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(0, 165, 66))));
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
|
||||
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
|
||||
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub struct Notification;
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(50))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification() {
|
||||
if event_tx().try_send(Notification).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
tracing::info!(
|
||||
"purging known borders: {:?}",
|
||||
borders.iter().map(|b| b.1.hwnd).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
for (_, border) in borders.iter() {
|
||||
border.destroy()?;
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
BORDERS_MONITORS.lock().clear();
|
||||
FOCUS_STATE.lock().clear();
|
||||
|
||||
let mut remaining_hwnds = vec![];
|
||||
|
||||
WindowsApi::enum_windows(
|
||||
Some(border_hwnds),
|
||||
&mut remaining_hwnds as *mut Vec<isize> as isize,
|
||||
)?;
|
||||
|
||||
if !remaining_hwnds.is_empty() {
|
||||
tracing::info!("purging unknown borders: {:?}", remaining_hwnds);
|
||||
|
||||
for hwnd in remaining_hwnds {
|
||||
Border::from(hwnd).destroy()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
||||
match focus_kind {
|
||||
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Stack => STACK.load(Ordering::SeqCst),
|
||||
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
let receiver = event_rx();
|
||||
event_tx().send(Notification)?;
|
||||
|
||||
let mut previous_snapshot = Ring::default();
|
||||
let mut previous_pending_move_op = None;
|
||||
let mut previous_is_paused = false;
|
||||
|
||||
'receiver: for _ in receiver {
|
||||
// Check the wm state every time we receive a notification
|
||||
let state = wm.lock();
|
||||
let is_paused = state.is_paused;
|
||||
let focused_monitor_idx = state.focused_monitor_idx();
|
||||
let monitors = state.monitors.clone();
|
||||
let pending_move_op = state.pending_move_op;
|
||||
drop(state);
|
||||
|
||||
match IMPLEMENTATION.load() {
|
||||
BorderImplementation::Windows => {
|
||||
'monitors: for (monitor_idx, m) in monitors.elements().iter().enumerate() {
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let window_kind = if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
};
|
||||
|
||||
monocle
|
||||
.focused_window()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
let window_kind = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
} else if c.windows().len() > 1 {
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
WindowKind::Single
|
||||
};
|
||||
|
||||
c.focused_window()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BorderImplementation::Komorebi => {
|
||||
let mut should_process_notification = true;
|
||||
|
||||
if monitors == previous_snapshot
|
||||
// handle the window dragging edge case
|
||||
&& pending_move_op == previous_pending_move_op
|
||||
{
|
||||
should_process_notification = false;
|
||||
}
|
||||
|
||||
// handle the pause edge case
|
||||
if is_paused && !previous_is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the unpause edge case
|
||||
if previous_is_paused && !is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the retile edge case
|
||||
if !should_process_notification && BORDER_STATE.lock().is_empty() {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
let mut borders_monitors = BORDERS_MONITORS.lock();
|
||||
|
||||
// If borders are disabled
|
||||
if !BORDER_ENABLED.load_consume()
|
||||
// Or if they are temporarily disabled
|
||||
|| BORDER_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
|
||||
// Or if the wm is paused
|
||||
|| is_paused
|
||||
// Or if we are handling an alt-tab across workspaces
|
||||
|| ALT_TAB_HWND.load().is_some()
|
||||
{
|
||||
// Destroy the borders we know about
|
||||
for (_, border) in borders.iter() {
|
||||
border.destroy()?;
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
|
||||
previous_is_paused = is_paused;
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
'monitors: for (monitor_idx, m) in monitors.elements().iter().enumerate() {
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Workspaces with tiling disabled don't have borders
|
||||
if !ws.tile() {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default()
|
||||
== monitor_idx
|
||||
{
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let border = match borders.entry(monocle.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(monocle.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(
|
||||
border.hwnd,
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
|
||||
border.update(&rect, true)?;
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
let mut to_remove = vec![];
|
||||
for (id, b) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default()
|
||||
== monitor_idx
|
||||
&& border_hwnd != b.hwnd
|
||||
{
|
||||
b.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let is_maximized = WindowsApi::is_zoomed(HWND(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
));
|
||||
|
||||
if is_maximized {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default()
|
||||
== monitor_idx
|
||||
{
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Destroy any borders not associated with the focused workspace
|
||||
let container_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
.map(|c| c.id().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
|
||||
&& !container_ids.contains(id)
|
||||
{
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
// Update border when moving or resizing with mouse
|
||||
if pending_move_op.is_some() && idx == ws.focused_container_idx() {
|
||||
let restore_z_order = Z_ORDER.load();
|
||||
Z_ORDER.store(ZOrder::TopMost);
|
||||
|
||||
let mut rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
|
||||
while WindowsApi::lbutton_is_pressed() {
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(c.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
|
||||
if rect != new_rect {
|
||||
rect = new_rect;
|
||||
border.update(&rect, true)?;
|
||||
}
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Get the border entry for this container from the map or create one
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(c.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(c.id().clone(), monitor_idx);
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
|
||||
let new_focus_state = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
} else if c.windows().len() > 1 {
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
WindowKind::Single
|
||||
};
|
||||
|
||||
// Update the focused state for all containers on this workspace
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
last_focus_state = focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
|
||||
let should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previous_snapshot = monitors;
|
||||
previous_pending_move_op = pending_move_op;
|
||||
previous_is_paused = is_paused;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum ZOrder {
|
||||
Top,
|
||||
NoTopMost,
|
||||
Bottom,
|
||||
TopMost,
|
||||
}
|
||||
|
||||
impl From<ZOrder> for isize {
|
||||
fn from(val: ZOrder) -> Self {
|
||||
match val {
|
||||
ZOrder::Top => 0,
|
||||
ZOrder::NoTopMost => -2,
|
||||
ZOrder::Bottom => 1,
|
||||
ZOrder::TopMost => -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ impl JsonSchema for Hex {
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
format: Some("color-hex".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
@@ -65,7 +66,7 @@ pub struct Rgb {
|
||||
}
|
||||
|
||||
impl Rgb {
|
||||
pub fn new(r: u32, g: u32, b: u32) -> Self {
|
||||
pub const fn new(r: u32, g: u32, b: u32) -> Self {
|
||||
Self { r, g, b }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,13 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::ring::Ring;
|
||||
use crate::stackbar::Stackbar;
|
||||
use crate::window::Window;
|
||||
use crate::StackbarMode;
|
||||
use crate::STACKBAR_MODE;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
|
||||
pub struct Container {
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
windows: Ring<Window>,
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
stackbar: Option<Stackbar>,
|
||||
}
|
||||
|
||||
impl_ring_elements!(Container, Window);
|
||||
@@ -28,10 +23,6 @@ impl Default for Container {
|
||||
Self {
|
||||
id: nanoid!(),
|
||||
windows: Ring::default(),
|
||||
stackbar: match *STACKBAR_MODE.lock() {
|
||||
StackbarMode::Always => Stackbar::create().ok(),
|
||||
StackbarMode::Never | StackbarMode::OnStack => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,10 +35,6 @@ impl PartialEq for Container {
|
||||
|
||||
impl Container {
|
||||
pub fn hide(&self, omit: Option<isize>) {
|
||||
if let Some(stackbar) = self.stackbar() {
|
||||
stackbar.hide();
|
||||
}
|
||||
|
||||
for window in self.windows().iter().rev() {
|
||||
let mut should_hide = omit.is_none();
|
||||
|
||||
@@ -66,10 +53,6 @@ impl Container {
|
||||
}
|
||||
|
||||
pub fn restore(&self) {
|
||||
if let Some(stackbar) = self.stackbar() {
|
||||
stackbar.restore();
|
||||
}
|
||||
|
||||
if let Some(window) = self.focused_window() {
|
||||
window.restore();
|
||||
}
|
||||
@@ -121,15 +104,7 @@ impl Container {
|
||||
|
||||
pub fn remove_window_by_idx(&mut self, idx: usize) -> Option<Window> {
|
||||
let window = self.windows_mut().remove(idx);
|
||||
|
||||
if matches!(*STACKBAR_MODE.lock(), StackbarMode::OnStack) && self.windows().len() <= 1 {
|
||||
self.stackbar = None;
|
||||
}
|
||||
|
||||
if idx != 0 {
|
||||
self.focus_window(idx - 1);
|
||||
};
|
||||
|
||||
self.focus_window(idx.saturating_sub(1));
|
||||
window
|
||||
}
|
||||
|
||||
@@ -140,15 +115,14 @@ impl Container {
|
||||
|
||||
pub fn add_window(&mut self, window: Window) {
|
||||
self.windows_mut().push_back(window);
|
||||
self.focus_window(self.windows().len().saturating_sub(1));
|
||||
let focused_window_idx = self.focused_window_idx();
|
||||
|
||||
if matches!(*STACKBAR_MODE.lock(), StackbarMode::OnStack)
|
||||
&& self.windows().len() > 1
|
||||
&& self.stackbar.is_none()
|
||||
{
|
||||
self.stackbar = Stackbar::create().ok();
|
||||
for (i, window) in self.windows().iter().enumerate() {
|
||||
if i != focused_window_idx {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
self.focus_window(self.windows().len() - 1);
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
|
||||
70
komorebi/src/focus_manager.rs
Normal file
70
komorebi/src/focus_manager.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use parking_lot::Mutex;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::Window;
|
||||
use crate::WindowManager;
|
||||
|
||||
pub struct Notification(isize);
|
||||
|
||||
impl Deref for Notification {
|
||||
type Target = isize;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
// Currently this should only be used for async focus updates, such as
|
||||
// when an animation finishes and we need to focus to set the cursor
|
||||
// position if the user has mouse follows focus enabled
|
||||
pub fn send_notification(hwnd: isize) {
|
||||
if event_tx().try_send(Notification(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
|
||||
for notification in receiver {
|
||||
let mouse_follows_focus = wm.lock().mouse_follows_focus;
|
||||
let _ = Window::from(*notification).focus(mouse_follows_focus);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use color_eyre::Result;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::FindWindowW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
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::WNDCLASSW;
|
||||
|
||||
use crate::windows_callbacks;
|
||||
use crate::WindowsApi;
|
||||
use crate::HIDDEN_HWND;
|
||||
use crate::TRANSPARENCY_COLOUR;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Hidden {
|
||||
pub(crate) hwnd: isize,
|
||||
}
|
||||
|
||||
impl From<isize> for Hidden {
|
||||
fn from(hwnd: isize) -> Self {
|
||||
Self { hwnd }
|
||||
}
|
||||
}
|
||||
|
||||
impl Hidden {
|
||||
pub const fn hwnd(self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn create(name: &str) -> Result<()> {
|
||||
let name: Vec<u16> = format!("{name}\0").encode_utf16().collect();
|
||||
let instance = WindowsApi::module_handle_w()?;
|
||||
let class_name = PCWSTR(name.as_ptr());
|
||||
let brush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
|
||||
let window_class = WNDCLASSW {
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(windows_callbacks::hidden_window),
|
||||
hbrBackground: brush,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _atom = WindowsApi::register_class_w(&window_class)?;
|
||||
|
||||
let name_cl = name.clone();
|
||||
std::thread::spawn(move || -> Result<()> {
|
||||
let hwnd = WindowsApi::create_hidden_window(PCWSTR(name_cl.as_ptr()), instance)?;
|
||||
let hidden = Self::from(hwnd);
|
||||
|
||||
let mut message = MSG::default();
|
||||
|
||||
unsafe {
|
||||
while GetMessageW(&mut message, hidden.hwnd(), 0, 0).into() {
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut hwnd = HWND(0);
|
||||
while hwnd == HWND(0) {
|
||||
hwnd = unsafe { FindWindowW(PCWSTR(name.as_ptr()), PCWSTR::null()) };
|
||||
}
|
||||
|
||||
HIDDEN_HWND.store(hwnd.0, Ordering::SeqCst);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
pub mod border;
|
||||
#![warn(clippy::all)]
|
||||
|
||||
pub mod animation;
|
||||
pub mod animation_manager;
|
||||
pub mod border_manager;
|
||||
pub mod com;
|
||||
#[macro_use]
|
||||
pub mod ring;
|
||||
pub mod colour;
|
||||
pub mod container;
|
||||
pub mod hidden;
|
||||
pub mod focus_manager;
|
||||
pub mod monitor;
|
||||
pub mod monitor_reconciliator;
|
||||
pub mod process_command;
|
||||
pub mod process_event;
|
||||
pub mod process_movement;
|
||||
pub mod reaper;
|
||||
pub mod set_window_position;
|
||||
pub mod stackbar;
|
||||
pub mod stackbar_manager;
|
||||
pub mod static_config;
|
||||
pub mod styles;
|
||||
pub mod transparency_manager;
|
||||
pub mod window;
|
||||
pub mod window_manager;
|
||||
pub mod window_manager_event;
|
||||
@@ -21,6 +28,7 @@ pub mod windows_callbacks;
|
||||
pub mod winevent;
|
||||
pub mod winevent_listener;
|
||||
pub mod workspace;
|
||||
pub mod workspace_reconciliator;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
@@ -32,16 +40,18 @@ use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicIsize;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use hidden::*;
|
||||
pub use animation::*;
|
||||
pub use animation_manager::*;
|
||||
pub use colour::*;
|
||||
pub use process_command::*;
|
||||
pub use process_event::*;
|
||||
pub use stackbar::*;
|
||||
pub use static_config::*;
|
||||
pub use window::*;
|
||||
pub use window_manager::*;
|
||||
pub use window_manager_event::*;
|
||||
pub use windows_api::WindowsApi;
|
||||
@@ -51,6 +61,7 @@ use color_eyre::Result;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::AnimationStyle;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
@@ -193,18 +204,16 @@ lazy_static! {
|
||||
)
|
||||
};
|
||||
|
||||
static ref ACTIVE_WINDOW_BORDER_STYLE: Arc<Mutex<ActiveWindowBorderStyle>> =
|
||||
Arc::new(Mutex::new(ActiveWindowBorderStyle::System));
|
||||
|
||||
static ref BORDER_RECT: Arc<Mutex<Rect>> =
|
||||
Arc::new(Mutex::new(Rect::default()));
|
||||
static ref ANIMATION_STYLE: Arc<Mutex<AnimationStyle >> =
|
||||
Arc::new(Mutex::new(AnimationStyle::Linear));
|
||||
|
||||
static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
|
||||
Arc::new(Mutex::new(AnimationManager::new()));
|
||||
|
||||
// Use app-specific titlebar removal options where possible
|
||||
// eg. Windows Terminal, IntelliJ IDEA, Firefox
|
||||
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
static ref STACKBAR_MODE: Arc<Mutex<StackbarMode >> = Arc::new(Mutex::new(StackbarMode::Never));
|
||||
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
@@ -216,27 +225,11 @@ pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
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);
|
||||
pub static BORDER_COLOUR_SINGLE: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_COLOUR_STACK: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_COLOUR_MONOCLE: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_COLOUR_CURRENT: AtomicU32 = AtomicU32::new(0);
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
|
||||
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
|
||||
|
||||
// 0 0 0 aka pure black, I doubt anyone will want this as a border colour
|
||||
pub const TRANSPARENCY_COLOUR: u32 = 0;
|
||||
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub static HIDDEN_HWND: AtomicIsize = AtomicIsize::new(0);
|
||||
|
||||
pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white
|
||||
pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947); // gray text
|
||||
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
|
||||
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
|
||||
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
|
||||
pub static ANIMATION_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
pub static ANIMATION_TEMPORARY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250);
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![warn(clippy::all)]
|
||||
#![allow(
|
||||
clippy::missing_errors_doc,
|
||||
clippy::redundant_pub_crate,
|
||||
clippy::significant_drop_tightening,
|
||||
clippy::significant_drop_in_scrutinee
|
||||
clippy::significant_drop_in_scrutinee,
|
||||
clippy::doc_markdown
|
||||
)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -23,22 +24,30 @@ use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use komorebi::hidden::Hidden;
|
||||
use komorebi::border_manager;
|
||||
use komorebi::focus_manager;
|
||||
use komorebi::load_configuration;
|
||||
use komorebi::monitor_reconciliator;
|
||||
use komorebi::process_command::listen_for_commands;
|
||||
use komorebi::process_command::listen_for_commands_tcp;
|
||||
use komorebi::process_event::listen_for_events;
|
||||
use komorebi::process_movement::listen_for_movements;
|
||||
use komorebi::reaper;
|
||||
use komorebi::stackbar_manager;
|
||||
use komorebi::static_config::StaticConfig;
|
||||
use komorebi::transparency_manager;
|
||||
use komorebi::window_manager::WindowManager;
|
||||
use komorebi::windows_api::WindowsApi;
|
||||
use komorebi::winevent_listener;
|
||||
use komorebi::workspace_reconciliator;
|
||||
use komorebi::CUSTOM_FFM;
|
||||
use komorebi::DATA_DIR;
|
||||
use komorebi::HOME_DIR;
|
||||
use komorebi::INITIAL_CONFIGURATION_LOADED;
|
||||
use komorebi::SESSION_ID;
|
||||
|
||||
shadow_rs::shadow!(build);
|
||||
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
@@ -50,8 +59,8 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
let appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi_plaintext.log");
|
||||
let color_appender = tracing_appender::rolling::never(std::env::temp_dir(), "komorebi.log");
|
||||
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
|
||||
let color_appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi.log");
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
|
||||
let (color_non_blocking, color_guard) = tracing_appender::non_blocking(color_appender);
|
||||
|
||||
@@ -126,7 +135,7 @@ fn detect_deadlocks() {
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
||||
struct Opts {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(short, long = "ffm")]
|
||||
@@ -186,8 +195,6 @@ fn main() -> Result<()> {
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
detect_deadlocks();
|
||||
|
||||
Hidden::create("komorebi-hidden")?;
|
||||
|
||||
let static_config = opts.config.map_or_else(
|
||||
|| {
|
||||
let komorebi_json = HOME_DIR.join("komorebi.json");
|
||||
@@ -253,6 +260,14 @@ fn main() -> Result<()> {
|
||||
listen_for_movements(wm.clone());
|
||||
}
|
||||
|
||||
border_manager::listen_for_notifications(wm.clone());
|
||||
stackbar_manager::listen_for_notifications(wm.clone());
|
||||
transparency_manager::listen_for_notifications(wm.clone());
|
||||
workspace_reconciliator::listen_for_notifications(wm.clone());
|
||||
monitor_reconciliator::listen_for_notifications(wm.clone())?;
|
||||
reaper::watch_for_orphans(wm.clone());
|
||||
focus_manager::listen_for_notifications(wm.clone());
|
||||
|
||||
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
||||
ctrlc::set_handler(move || {
|
||||
ctrlc_sender
|
||||
|
||||
@@ -19,7 +19,16 @@ use crate::ring::Ring;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
|
||||
Debug,
|
||||
Clone,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Getters,
|
||||
CopyGetters,
|
||||
MutGetters,
|
||||
Setters,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
pub struct Monitor {
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
@@ -27,15 +36,19 @@ pub struct Monitor {
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
name: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
device: Option<String>,
|
||||
device: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
device_id: Option<String>,
|
||||
device_id: String,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
size: Rect,
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
work_area_size: Rect,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
work_area_offset: Option<Rect>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
window_based_work_area_offset: Option<Rect>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
window_based_work_area_offset_limit: isize,
|
||||
workspaces: Ring<Workspace>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
@@ -46,18 +59,27 @@ pub struct Monitor {
|
||||
|
||||
impl_ring_elements!(Monitor, Workspace);
|
||||
|
||||
pub fn new(id: isize, size: Rect, work_area_size: Rect, name: String) -> Monitor {
|
||||
pub fn new(
|
||||
id: isize,
|
||||
size: Rect,
|
||||
work_area_size: Rect,
|
||||
name: String,
|
||||
device: String,
|
||||
device_id: String,
|
||||
) -> Monitor {
|
||||
let mut workspaces = Ring::default();
|
||||
workspaces.elements_mut().push_back(Workspace::default());
|
||||
|
||||
Monitor {
|
||||
id,
|
||||
name,
|
||||
device: None,
|
||||
device_id: None,
|
||||
device,
|
||||
device_id,
|
||||
size,
|
||||
work_area_size,
|
||||
work_area_offset: None,
|
||||
window_based_work_area_offset: None,
|
||||
window_based_work_area_offset_limit: 1,
|
||||
workspaces,
|
||||
last_focused_workspace: None,
|
||||
workspace_names: HashMap::default(),
|
||||
@@ -105,7 +127,7 @@ impl Monitor {
|
||||
if idx == 0 {
|
||||
self.workspaces_mut().push_back(Workspace::default());
|
||||
} else {
|
||||
self.focus_workspace(idx - 1).ok()?;
|
||||
self.focus_workspace(idx.saturating_sub(1)).ok()?;
|
||||
};
|
||||
|
||||
None
|
||||
@@ -194,6 +216,11 @@ impl Monitor {
|
||||
|
||||
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> Result<()> {
|
||||
let work_area = *self.work_area_size();
|
||||
let window_based_work_area_offset = (
|
||||
self.window_based_work_area_offset_limit(),
|
||||
self.window_based_work_area_offset(),
|
||||
);
|
||||
|
||||
let offset = if self.work_area_offset().is_some() {
|
||||
self.work_area_offset()
|
||||
} else {
|
||||
@@ -202,7 +229,7 @@ impl Monitor {
|
||||
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
.update(&work_area, offset)?;
|
||||
.update(&work_area, offset, window_based_work_area_offset)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
203
komorebi/src/monitor_reconciliator/hidden.rs
Normal file
203
komorebi/src/monitor_reconciliator/hidden.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PBT_APMSUSPEND;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_POWERBROADCAST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_WTSSESSION_CHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
|
||||
|
||||
use crate::monitor_reconciliator;
|
||||
use crate::WindowsApi;
|
||||
|
||||
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Hidden {
|
||||
pub hwnd: isize,
|
||||
}
|
||||
|
||||
impl From<isize> for Hidden {
|
||||
fn from(hwnd: isize) -> Self {
|
||||
Self { hwnd }
|
||||
}
|
||||
}
|
||||
|
||||
impl Hidden {
|
||||
pub const fn hwnd(self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn create(name: &str) -> color_eyre::Result<Self> {
|
||||
let name: Vec<u16> = format!("{name}\0").encode_utf16().collect();
|
||||
let class_name = PCWSTR(name.as_ptr());
|
||||
|
||||
let h_module = WindowsApi::module_handle_w()?;
|
||||
let window_class = WNDCLASSW {
|
||||
hInstance: h_module.into(),
|
||||
lpszClassName: class_name,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(Self::callback),
|
||||
hbrBackground: WindowsApi::create_solid_brush(0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _ = WindowsApi::register_class_w(&window_class)?;
|
||||
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
|
||||
std::thread::spawn(move || -> color_eyre::Result<()> {
|
||||
let hwnd = WindowsApi::create_hidden_window(PCWSTR(name.as_ptr()), h_module)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
|
||||
let mut msg: MSG = MSG::default();
|
||||
|
||||
loop {
|
||||
unsafe {
|
||||
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
|
||||
tracing::debug!("hidden window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let hwnd = hwnd_receiver.recv()?;
|
||||
|
||||
WindowsApi::wts_register_session_notification(hwnd)?;
|
||||
|
||||
Ok(Self { hwnd })
|
||||
}
|
||||
|
||||
pub extern "system" fn callback(
|
||||
window: HWND,
|
||||
message: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_POWERBROADCAST => {
|
||||
match wparam.0 as u32 {
|
||||
// Automatic: System resumed itself from sleep or hibernation
|
||||
// Suspend: User resumed system from sleep or hibernation
|
||||
PBT_APMRESUMEAUTOMATIC | PBT_APMRESUMESUSPEND => {
|
||||
tracing::debug!(
|
||||
"WM_POWERBROADCAST event received - resume from suspend"
|
||||
);
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::ResumingFromSuspendedState,
|
||||
);
|
||||
LRESULT(0)
|
||||
}
|
||||
// Computer is entering a suspended state
|
||||
PBT_APMSUSPEND => {
|
||||
tracing::debug!(
|
||||
"WM_POWERBROADCAST event received - entering suspended state"
|
||||
);
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::EnteringSuspendedState,
|
||||
);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => LRESULT(0),
|
||||
}
|
||||
}
|
||||
WM_WTSSESSION_CHANGE => {
|
||||
match wparam.0 as u32 {
|
||||
WTS_SESSION_LOCK => {
|
||||
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked");
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::SessionLocked,
|
||||
);
|
||||
}
|
||||
WTS_SESSION_UNLOCK => {
|
||||
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked");
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::SessionUnlocked,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
// This event gets sent when:
|
||||
// - The scaling factor on a display changes
|
||||
// - The resolution on a display changes
|
||||
// - A monitor is added
|
||||
// - A monitor is removed
|
||||
// Since WM_DEVICECHANGE also notifies on monitor changes, we only handle scaling
|
||||
// and resolution changes here
|
||||
WM_DISPLAYCHANGE => {
|
||||
tracing::debug!(
|
||||
"WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed", wparam.0
|
||||
);
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::ResolutionScalingChanged,
|
||||
);
|
||||
LRESULT(0)
|
||||
}
|
||||
// Unfortunately this is the event sent with ButteryTaskbar which I use a lot
|
||||
// Original idea from https://stackoverflow.com/a/33762334
|
||||
WM_SETTINGCHANGE => {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
if wparam.0 as u32 == SPI_SETWORKAREA.0 {
|
||||
tracing::debug!(
|
||||
"WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)"
|
||||
);
|
||||
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::WorkAreaChanged,
|
||||
);
|
||||
}
|
||||
LRESULT(0)
|
||||
}
|
||||
// This event + wparam combo is sent 4 times when a monitor is added based on my testing on win11
|
||||
// Original idea from https://stackoverflow.com/a/33762334
|
||||
WM_DEVICECHANGE => {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
|
||||
tracing::debug!(
|
||||
"WM_DEVICECHANGE event received with DBT_DEVNODES_CHANGED - display added or removed"
|
||||
);
|
||||
monitor_reconciliator::send_notification(
|
||||
monitor_reconciliator::Notification::DisplayConnectionChange,
|
||||
);
|
||||
}
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(window, message, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
416
komorebi/src/monitor_reconciliator/mod.rs
Normal file
416
komorebi/src/monitor_reconciliator/mod.rs
Normal file
@@ -0,0 +1,416 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::monitor;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::monitor_reconciliator::hidden::Hidden;
|
||||
use crate::MonitorConfig;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::Rect;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub mod hidden;
|
||||
|
||||
pub enum Notification {
|
||||
ResolutionScalingChanged,
|
||||
WorkAreaChanged,
|
||||
DisplayConnectionChange,
|
||||
EnteringSuspendedState,
|
||||
ResumingFromSuspendedState,
|
||||
SessionLocked,
|
||||
SessionUnlocked,
|
||||
}
|
||||
|
||||
static ACTIVE: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
static MONITOR_CACHE: OnceLock<Mutex<HashMap<String, MonitorConfig>>> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification(notification: Notification) {
|
||||
if event_tx().try_send(notification).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_in_monitor_cache(device_id: &str, config: MonitorConfig) {
|
||||
let mut monitor_cache = MONITOR_CACHE
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock();
|
||||
|
||||
monitor_cache.insert(device_id.to_string(), config);
|
||||
}
|
||||
|
||||
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
|
||||
Ok(win32_display_data::connected_displays_all()
|
||||
.flatten()
|
||||
.map(|display| {
|
||||
let path = display.device_path;
|
||||
|
||||
let (device, device_id) = if path.is_empty() {
|
||||
(String::from("UNKNOWN"), String::from("UNKNOWN"))
|
||||
} else {
|
||||
let mut split: Vec<_> = path.split('#').collect();
|
||||
split.remove(0);
|
||||
split.remove(split.len() - 1);
|
||||
let device = split[0].to_string();
|
||||
let device_id = split.join("-");
|
||||
(device, device_id)
|
||||
};
|
||||
|
||||
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
|
||||
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
|
||||
|
||||
monitor::new(
|
||||
display.hmonitor,
|
||||
display.size.into(),
|
||||
display.work_area_size.into(),
|
||||
name,
|
||||
device,
|
||||
device_id,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
#[allow(clippy::expect_used)]
|
||||
Hidden::create("komorebi-hidden")?;
|
||||
|
||||
tracing::info!("created hidden window to listen for monitor-related events");
|
||||
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!("restarting failed thread: {:?}", error)
|
||||
} else {
|
||||
tracing::error!("restarting failed thread: {}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
|
||||
'receiver: for notification in receiver {
|
||||
if !ACTIVE.load_consume() {
|
||||
if matches!(
|
||||
notification,
|
||||
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked
|
||||
) {
|
||||
tracing::debug!(
|
||||
"reactivating reconciliator - system has resumed from suspended state or session has been unlocked"
|
||||
);
|
||||
|
||||
ACTIVE.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
let mut wm = wm.lock();
|
||||
|
||||
match notification {
|
||||
Notification::EnteringSuspendedState | Notification::SessionLocked => {
|
||||
tracing::debug!(
|
||||
"deactivating reconciliator until system resumes from suspended state or session is unlocked"
|
||||
);
|
||||
ACTIVE.store(false, Ordering::SeqCst);
|
||||
}
|
||||
Notification::ResumingFromSuspendedState | Notification::SessionUnlocked => {
|
||||
// this is only handled above if the reconciliator is paused
|
||||
}
|
||||
Notification::WorkAreaChanged => {
|
||||
tracing::debug!("handling work area changed notification");
|
||||
let offset = wm.work_area_offset;
|
||||
for monitor in wm.monitors_mut() {
|
||||
let mut should_update = false;
|
||||
|
||||
// Update work areas as necessary
|
||||
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
|
||||
if reference.work_area_size() != monitor.work_area_size() {
|
||||
monitor.set_work_area_size(Rect {
|
||||
left: reference.work_area_size().left,
|
||||
top: reference.work_area_size().top,
|
||||
right: reference.work_area_size().right,
|
||||
bottom: reference.work_area_size().bottom,
|
||||
});
|
||||
|
||||
should_update = true;
|
||||
}
|
||||
}
|
||||
|
||||
if should_update {
|
||||
tracing::info!("updated work area for {}", monitor.device_id());
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
border_manager::send_notification();
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"work areas match, reconciliation not required for {}",
|
||||
monitor.device_id()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Notification::ResolutionScalingChanged => {
|
||||
tracing::debug!("handling resolution/scaling changed notification");
|
||||
let offset = wm.work_area_offset;
|
||||
for monitor in wm.monitors_mut() {
|
||||
let mut should_update = false;
|
||||
|
||||
// Update sizes and work areas as necessary
|
||||
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
|
||||
if reference.work_area_size() != monitor.work_area_size() {
|
||||
monitor.set_work_area_size(Rect {
|
||||
left: reference.work_area_size().left,
|
||||
top: reference.work_area_size().top,
|
||||
right: reference.work_area_size().right,
|
||||
bottom: reference.work_area_size().bottom,
|
||||
});
|
||||
|
||||
should_update = true;
|
||||
}
|
||||
|
||||
if reference.size() != monitor.size() {
|
||||
monitor.set_size(Rect {
|
||||
left: reference.size().left,
|
||||
top: reference.size().top,
|
||||
right: reference.size().right,
|
||||
bottom: reference.size().bottom,
|
||||
});
|
||||
|
||||
should_update = true;
|
||||
}
|
||||
}
|
||||
|
||||
if should_update {
|
||||
tracing::info!(
|
||||
"updated monitor resolution/scaling for {}",
|
||||
monitor.device_id()
|
||||
);
|
||||
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
border_manager::send_notification();
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"resolutions match, reconciliation not required for {}",
|
||||
monitor.device_id()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Notification::DisplayConnectionChange => {
|
||||
tracing::debug!("handling display connection change notification");
|
||||
let mut monitor_cache = MONITOR_CACHE
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock();
|
||||
|
||||
let initial_monitor_count = wm.monitors().len();
|
||||
|
||||
// Get the currently attached display devices
|
||||
let attached_devices = attached_display_devices()?;
|
||||
|
||||
// Make sure that in our state any attached displays have the latest Win32 data
|
||||
for monitor in wm.monitors_mut() {
|
||||
for attached in &attached_devices {
|
||||
if attached.device_id().eq(monitor.device_id()) {
|
||||
monitor.set_id(attached.id());
|
||||
monitor.set_name(attached.name().clone());
|
||||
monitor.set_size(*attached.size());
|
||||
monitor.set_work_area_size(*attached.work_area_size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if initial_monitor_count == attached_devices.len() {
|
||||
tracing::debug!("monitor counts match, reconciliation not required");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
if attached_devices.is_empty() {
|
||||
tracing::debug!(
|
||||
"no devices found, skipping reconciliation to avoid breaking state"
|
||||
);
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
if initial_monitor_count > attached_devices.len() {
|
||||
tracing::info!(
|
||||
"monitor count mismatch ({initial_monitor_count} vs {}), removing disconnected monitors",
|
||||
attached_devices.len()
|
||||
);
|
||||
|
||||
// Gather all the containers that will be orphaned from disconnected and invalid displays
|
||||
let mut orphaned_containers = vec![];
|
||||
|
||||
// Collect the ids in our state which aren't in the current attached display ids
|
||||
// These are monitors that have been removed
|
||||
let mut newly_removed_displays = vec![];
|
||||
|
||||
for m in wm.monitors().iter() {
|
||||
if !attached_devices
|
||||
.iter()
|
||||
.any(|attached| attached.device_id().eq(m.device_id()))
|
||||
{
|
||||
newly_removed_displays.push(m.device_id().clone());
|
||||
for workspace in m.workspaces() {
|
||||
for container in workspace.containers() {
|
||||
// Save the orphaned containers from the removed monitor
|
||||
orphaned_containers.push(container.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Let's add their state to the cache for later
|
||||
monitor_cache.insert(m.device_id().clone(), m.into());
|
||||
}
|
||||
}
|
||||
|
||||
if !orphaned_containers.is_empty() {
|
||||
tracing::info!(
|
||||
"removed orphaned containers from: {newly_removed_displays:?}"
|
||||
);
|
||||
}
|
||||
|
||||
if !newly_removed_displays.is_empty() {
|
||||
// After we have cached them, remove them from our state
|
||||
wm.monitors_mut()
|
||||
.retain(|m| !newly_removed_displays.contains(m.device_id()));
|
||||
}
|
||||
|
||||
let post_removal_monitor_count = wm.monitors().len();
|
||||
let focused_monitor_idx = wm.focused_monitor_idx();
|
||||
if focused_monitor_idx >= post_removal_monitor_count {
|
||||
wm.focus_monitor(0)?;
|
||||
}
|
||||
|
||||
if !orphaned_containers.is_empty() {
|
||||
if let Some(primary) = wm.monitors_mut().front_mut() {
|
||||
if let Some(focused_ws) = primary.focused_workspace_mut() {
|
||||
let focused_container_idx = focused_ws.focused_container_idx();
|
||||
|
||||
// Put the orphaned containers somewhere visible
|
||||
for container in orphaned_containers {
|
||||
focused_ws.add_container(container);
|
||||
}
|
||||
|
||||
// Gotta reset the focus or the movement will feel "off"
|
||||
if initial_monitor_count != post_removal_monitor_count {
|
||||
focused_ws.focus_container(focused_container_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let offset = wm.work_area_offset;
|
||||
|
||||
for monitor in wm.monitors_mut() {
|
||||
// If we have lost a monitor, update everything to filter out any jank
|
||||
if initial_monitor_count != post_removal_monitor_count {
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let post_removal_monitor_count = wm.monitors().len();
|
||||
|
||||
// This is the list of device ids after we have removed detached displays
|
||||
let post_removal_device_ids = wm
|
||||
.monitors()
|
||||
.iter()
|
||||
.map(Monitor::device_id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Check for and add any new monitors that may have been plugged in
|
||||
// Monitor and display index preferences get applied in this function
|
||||
WindowsApi::load_monitor_information(&mut wm.monitors)?;
|
||||
|
||||
let post_addition_monitor_count = wm.monitors().len();
|
||||
|
||||
if post_addition_monitor_count > post_removal_monitor_count {
|
||||
tracing::info!(
|
||||
"monitor count mismatch ({post_removal_monitor_count} vs {post_addition_monitor_count}), adding connected monitors",
|
||||
);
|
||||
|
||||
// Look in the updated state for new monitors
|
||||
for m in wm.monitors_mut() {
|
||||
let device_id = m.device_id().clone();
|
||||
// We identify a new monitor when we encounter a new device id
|
||||
if !post_removal_device_ids.contains(&device_id) {
|
||||
let mut cache_hit = false;
|
||||
// Check if that device id exists in the cache for this session
|
||||
if let Some(cached) = monitor_cache.get(&device_id) {
|
||||
cache_hit = true;
|
||||
|
||||
tracing::info!("found monitor and workspace configuration for {device_id} in the monitor cache, applying");
|
||||
|
||||
// If it does, load all the monitor settings from the cache entry
|
||||
m.ensure_workspace_count(cached.workspaces.len());
|
||||
m.set_work_area_offset(cached.work_area_offset);
|
||||
m.set_window_based_work_area_offset(
|
||||
cached.window_based_work_area_offset,
|
||||
);
|
||||
m.set_window_based_work_area_offset_limit(
|
||||
cached.window_based_work_area_offset_limit.unwrap_or(1),
|
||||
);
|
||||
|
||||
for (w_idx, workspace) in m.workspaces_mut().iter_mut().enumerate()
|
||||
{
|
||||
if let Some(cached_workspace) = cached.workspaces.get(w_idx) {
|
||||
workspace.load_static_config(cached_workspace)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Entries in the cache should only be used once; remove the entry there was a cache hit
|
||||
if cache_hit {
|
||||
monitor_cache.remove(&device_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let final_count = wm.monitors().len();
|
||||
|
||||
if post_removal_monitor_count != final_count {
|
||||
wm.retile_all(true)?;
|
||||
// Second retile to fix DPI/resolution related jank
|
||||
wm.retile_all(true)?;
|
||||
// Border updates to fix DPI/resolution related jank
|
||||
border_manager::send_notification();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -27,6 +27,7 @@ use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::Axis;
|
||||
use komorebi_core::BorderImplementation;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::MoveBehaviour;
|
||||
@@ -38,26 +39,29 @@ use komorebi_core::StateQuery;
|
||||
use komorebi_core::WindowContainerBehaviour;
|
||||
use komorebi_core::WindowKind;
|
||||
|
||||
use crate::border::Border;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::IMPLEMENTATION;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::colour::Rgb;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::notify_subscribers;
|
||||
use crate::stackbar_manager;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::transparency_manager;
|
||||
use crate::window::RuleDebug;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::GlobalState;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::BORDER_COLOUR_CURRENT;
|
||||
use crate::BORDER_COLOUR_MONOCLE;
|
||||
use crate::BORDER_COLOUR_SINGLE;
|
||||
use crate::BORDER_COLOUR_STACK;
|
||||
use crate::BORDER_ENABLED;
|
||||
use crate::BORDER_HIDDEN;
|
||||
use crate::BORDER_HWND;
|
||||
use crate::BORDER_OFFSET;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_ENABLED;
|
||||
use crate::ANIMATION_FPS;
|
||||
use crate::ANIMATION_STYLE;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
@@ -74,30 +78,45 @@ use crate::SUBSCRIPTION_PIPES;
|
||||
use crate::SUBSCRIPTION_SOCKETS;
|
||||
use crate::TCP_CONNECTIONS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WINDOWS_11;
|
||||
use crate::WORKSPACE_RULES;
|
||||
use stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use stackbar_manager::STACKBAR_LABEL;
|
||||
use stackbar_manager::STACKBAR_MODE;
|
||||
use stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use stackbar_manager::STACKBAR_TAB_HEIGHT;
|
||||
use stackbar_manager::STACKBAR_TAB_WIDTH;
|
||||
use stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
|
||||
let listener = wm
|
||||
.lock()
|
||||
.command_listener
|
||||
.try_clone()
|
||||
.expect("could not clone unix listener");
|
||||
std::thread::spawn(move || loop {
|
||||
let wm = wm.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
tracing::info!("listening on komorebi.sock");
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(stream) => match read_commands_uds(&wm, stream) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
},
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error);
|
||||
break;
|
||||
let _ = std::thread::spawn(move || {
|
||||
let listener = wm
|
||||
.lock()
|
||||
.command_listener
|
||||
.try_clone()
|
||||
.expect("could not clone unix listener");
|
||||
|
||||
tracing::info!("listening on komorebi.sock");
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(stream) => match read_commands_uds(&wm, stream) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
},
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.join();
|
||||
|
||||
tracing::error!("restarting failed thread");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,21 +184,6 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
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()?;
|
||||
BORDER_HIDDEN.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
match message {
|
||||
SocketMessage::CycleFocusWorkspace(_) | SocketMessage::FocusWorkspaceNumber(_) => {
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
@@ -200,6 +204,10 @@ impl WindowManager {
|
||||
match message {
|
||||
SocketMessage::Promote => self.promote_container_to_front()?,
|
||||
SocketMessage::PromoteFocus => self.promote_focus_to_front()?,
|
||||
SocketMessage::PromoteWindow(direction) => {
|
||||
self.focus_container_in_direction(direction)?;
|
||||
self.promote_container_to_front()?
|
||||
}
|
||||
SocketMessage::FocusWindow(direction) => {
|
||||
self.focus_container_in_direction(direction)?;
|
||||
}
|
||||
@@ -214,6 +222,8 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
|
||||
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
|
||||
SocketMessage::StackAll => self.stack_all()?,
|
||||
SocketMessage::UnstackAll => self.unstack_all()?,
|
||||
SocketMessage::CycleStack(direction) => {
|
||||
self.cycle_container_window_in_direction(direction)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
@@ -224,8 +234,12 @@ impl WindowManager {
|
||||
WindowsApi::center_cursor_in_rect(&focused_window_rect)?;
|
||||
WindowsApi::left_click();
|
||||
}
|
||||
SocketMessage::Close => self.focused_window()?.close()?,
|
||||
SocketMessage::Minimize => self.focused_window()?.minimize(),
|
||||
SocketMessage::Close => {
|
||||
Window::from(WindowsApi::foreground_window()?).close()?;
|
||||
}
|
||||
SocketMessage::Minimize => {
|
||||
Window::from(WindowsApi::foreground_window()?).minimize();
|
||||
}
|
||||
SocketMessage::ToggleFloat => self.toggle_float()?,
|
||||
SocketMessage::ToggleMonocle => self.toggle_monocle()?,
|
||||
SocketMessage::ToggleMaximize => self.toggle_maximize()?,
|
||||
@@ -269,6 +283,40 @@ impl WindowManager {
|
||||
self.handle_definitive_workspace_rules(id, monitor_idx, workspace_idx)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::ClearWorkspaceRules(monitor_idx, workspace_idx) => {
|
||||
let mut workspace_rules = WORKSPACE_RULES.lock();
|
||||
let mut to_remove = vec![];
|
||||
for (id, (m_idx, w_idx, _)) in workspace_rules.iter() {
|
||||
if monitor_idx == *m_idx && workspace_idx == *w_idx {
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for rule in to_remove {
|
||||
workspace_rules.remove(&rule);
|
||||
}
|
||||
}
|
||||
SocketMessage::ClearNamedWorkspaceRules(ref workspace) => {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
self.monitor_workspace_index_by_name(workspace)
|
||||
{
|
||||
let mut workspace_rules = WORKSPACE_RULES.lock();
|
||||
let mut to_remove = vec![];
|
||||
for (id, (m_idx, w_idx, _)) in workspace_rules.iter() {
|
||||
if monitor_idx == *m_idx && workspace_idx == *w_idx {
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for rule in to_remove {
|
||||
workspace_rules.remove(&rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::ClearAllWorkspaceRules => {
|
||||
let mut workspace_rules = WORKSPACE_RULES.lock();
|
||||
workspace_rules.clear();
|
||||
}
|
||||
SocketMessage::ManageRule(identifier, ref id) => {
|
||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
|
||||
@@ -455,6 +503,9 @@ impl WindowManager {
|
||||
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
|
||||
}
|
||||
SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
|
||||
}
|
||||
SocketMessage::SendContainerToNamedWorkspace(ref workspace) => {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
self.monitor_workspace_index_by_name(workspace)
|
||||
@@ -477,6 +528,15 @@ impl WindowManager {
|
||||
SocketMessage::MoveWorkspaceToMonitorNumber(monitor_idx) => {
|
||||
self.move_workspace_to_monitor(monitor_idx)?;
|
||||
}
|
||||
SocketMessage::CycleMoveWorkspaceToMonitor(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.move_workspace_to_monitor(monitor_idx)?;
|
||||
}
|
||||
SocketMessage::TogglePause => {
|
||||
if self.is_paused {
|
||||
tracing::info!("resuming");
|
||||
@@ -498,13 +558,17 @@ impl WindowManager {
|
||||
);
|
||||
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
self.update_focused_workspace(self.mouse_follows_focus)?;
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
||||
}
|
||||
SocketMessage::FocusMonitorNumber(monitor_idx) => {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
self.update_focused_workspace(self.mouse_follows_focus)?;
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
||||
}
|
||||
SocketMessage::Retile => {
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
border_manager::destroy_all_borders()?;
|
||||
self.retile_all(false)?
|
||||
}
|
||||
SocketMessage::Retile => self.retile_all(false)?,
|
||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
||||
@@ -627,10 +691,6 @@ impl WindowManager {
|
||||
);
|
||||
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
|
||||
if BORDER_ENABLED.load(Ordering::SeqCst) {
|
||||
self.show_border()?;
|
||||
};
|
||||
}
|
||||
SocketMessage::FocusLastWorkspace => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
@@ -654,10 +714,6 @@ impl WindowManager {
|
||||
self.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.set_last_focused_workspace(Option::from(idx));
|
||||
|
||||
if BORDER_ENABLED.load(Ordering::SeqCst) {
|
||||
self.show_border()?;
|
||||
};
|
||||
}
|
||||
SocketMessage::FocusWorkspaceNumber(workspace_idx) => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
@@ -667,11 +723,9 @@ impl WindowManager {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
|
||||
if BORDER_ENABLED.load(Ordering::SeqCst) {
|
||||
self.show_border()?;
|
||||
};
|
||||
if self.focused_workspace_idx().unwrap_or_default() != workspace_idx {
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusWorkspaceNumbers(workspace_idx) => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
@@ -691,14 +745,17 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
|
||||
if BORDER_ENABLED.load(Ordering::SeqCst) {
|
||||
self.show_border()?;
|
||||
};
|
||||
}
|
||||
SocketMessage::FocusMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
|
||||
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
if focused_pair != (monitor_idx, workspace_idx) {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusNamedWorkspace(ref name) => {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
@@ -707,10 +764,6 @@ impl WindowManager {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
|
||||
if BORDER_ENABLED.load(Ordering::SeqCst) {
|
||||
self.show_border()?;
|
||||
};
|
||||
}
|
||||
SocketMessage::Stop => {
|
||||
tracing::info!(
|
||||
@@ -765,30 +818,46 @@ impl WindowManager {
|
||||
|
||||
tracing::info!("replying to state done");
|
||||
}
|
||||
SocketMessage::GlobalState => {
|
||||
let state = match serde_json::to_string_pretty(&GlobalState::default()) {
|
||||
Ok(state) => state,
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
|
||||
tracing::info!("replying to global state");
|
||||
|
||||
reply.write_all(state.as_bytes())?;
|
||||
|
||||
tracing::info!("replying to global state done");
|
||||
}
|
||||
SocketMessage::VisibleWindows => {
|
||||
let mut monitor_visible_windows = HashMap::new();
|
||||
|
||||
for (index, monitor) in self.monitors().iter().enumerate() {
|
||||
for monitor in self.monitors() {
|
||||
if let Some(ws) = monitor.focused_workspace() {
|
||||
monitor_visible_windows.insert(
|
||||
monitor
|
||||
.device_id()
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{index}")),
|
||||
monitor.device_id().clone(),
|
||||
ws.visible_window_details().clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let visible_windows_state =
|
||||
match serde_json::to_string_pretty(&monitor_visible_windows) {
|
||||
Ok(state) => state,
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
let visible_windows_state = serde_json::to_string_pretty(&monitor_visible_windows)
|
||||
.unwrap_or_else(|error| error.to_string());
|
||||
|
||||
reply.write_all(visible_windows_state.as_bytes())?;
|
||||
}
|
||||
SocketMessage::MonitorInformation => {
|
||||
let mut monitors = HashMap::new();
|
||||
for monitor in self.monitors() {
|
||||
monitors.insert(monitor.device_id(), monitor.size());
|
||||
}
|
||||
|
||||
let monitors_state = serde_json::to_string_pretty(&monitors)
|
||||
.unwrap_or_else(|error| error.to_string());
|
||||
|
||||
reply.write_all(monitors_state.as_bytes())?;
|
||||
}
|
||||
SocketMessage::Query(query) => {
|
||||
let response = match query {
|
||||
StateQuery::FocusedMonitorIndex => self.focused_monitor_idx(),
|
||||
@@ -913,7 +982,7 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::FocusFollowsMouse(mut implementation, enable) => {
|
||||
if !CUSTOM_FFM.load(Ordering::SeqCst) {
|
||||
@@ -1019,7 +1088,7 @@ impl WindowManager {
|
||||
SocketMessage::CompleteConfiguration => {
|
||||
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
||||
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::WatchConfiguration(enable) => {
|
||||
@@ -1126,7 +1195,7 @@ impl WindowManager {
|
||||
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
||||
|
||||
workspace.set_resize_dimensions(resize);
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::Save(ref path) => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
@@ -1149,7 +1218,7 @@ impl WindowManager {
|
||||
let resize: Vec<Option<Rect>> = serde_json::from_reader(file)?;
|
||||
|
||||
workspace.set_resize_dimensions(resize);
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::AddSubscriberSocket(ref socket) => {
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
@@ -1204,6 +1273,7 @@ impl WindowManager {
|
||||
MoveBehaviour::Insert => {
|
||||
self.cross_monitor_move_behaviour = MoveBehaviour::Swap;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
SocketMessage::CrossMonitorMoveBehaviour(behaviour) => {
|
||||
@@ -1212,42 +1282,99 @@ impl WindowManager {
|
||||
SocketMessage::UnmanagedWindowOperationBehaviour(behaviour) => {
|
||||
self.unmanaged_window_operation_behaviour = behaviour;
|
||||
}
|
||||
SocketMessage::ActiveWindowBorder(enable) => {
|
||||
if enable {
|
||||
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
|
||||
Border::create("komorebi-border-window")?;
|
||||
}
|
||||
|
||||
BORDER_ENABLED.store(true, Ordering::SeqCst);
|
||||
self.show_border()?;
|
||||
SocketMessage::Border(enable) => {
|
||||
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::BorderImplementation(implementation) => {
|
||||
if !*WINDOWS_11 && matches!(implementation, BorderImplementation::Windows) {
|
||||
tracing::error!(
|
||||
"BorderImplementation::Windows is only supported on Windows 11 and above"
|
||||
);
|
||||
} else {
|
||||
BORDER_ENABLED.store(false, Ordering::SeqCst);
|
||||
self.hide_border()?;
|
||||
}
|
||||
}
|
||||
SocketMessage::ActiveWindowBorderColour(kind, r, g, b) => {
|
||||
match kind {
|
||||
WindowKind::Single => {
|
||||
BORDER_COLOUR_SINGLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
BORDER_COLOUR_CURRENT.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
IMPLEMENTATION.store(implementation);
|
||||
match IMPLEMENTATION.load() {
|
||||
BorderImplementation::Komorebi => {
|
||||
self.remove_all_accents()?;
|
||||
}
|
||||
BorderImplementation::Windows => {
|
||||
border_manager::destroy_all_borders()?;
|
||||
}
|
||||
}
|
||||
WindowKind::Stack => {
|
||||
BORDER_COLOUR_STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Monocle => {
|
||||
BORDER_COLOUR_MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
border_manager::send_notification();
|
||||
}
|
||||
}
|
||||
SocketMessage::ActiveWindowBorderWidth(width) => {
|
||||
BORDER_WIDTH.store(width, Ordering::SeqCst);
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
SocketMessage::BorderColour(kind, r, g, b) => match kind {
|
||||
WindowKind::Single => {
|
||||
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Stack => {
|
||||
border_manager::STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Monocle => {
|
||||
border_manager::MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Unfocused => {
|
||||
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
},
|
||||
SocketMessage::BorderStyle(style) => {
|
||||
STYLE.store(style);
|
||||
}
|
||||
SocketMessage::ActiveWindowBorderOffset(offset) => {
|
||||
BORDER_OFFSET.store(offset, Ordering::SeqCst);
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
SocketMessage::BorderWidth(width) => {
|
||||
border_manager::BORDER_WIDTH.store(width, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::BorderOffset(offset) => {
|
||||
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::Animation(enable) => {
|
||||
ANIMATION_ENABLED.store(enable, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::AnimationDuration(duration) => {
|
||||
ANIMATION_DURATION.store(duration, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::AnimationFps(fps) => {
|
||||
ANIMATION_FPS.store(fps, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::AnimationStyle(style) => {
|
||||
*ANIMATION_STYLE.lock() = style;
|
||||
}
|
||||
SocketMessage::Transparency(enable) => {
|
||||
transparency_manager::TRANSPARENCY_ENABLED.store(enable, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::TransparencyAlpha(alpha) => {
|
||||
transparency_manager::TRANSPARENCY_ALPHA.store(alpha, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::StackbarMode(mode) => {
|
||||
STACKBAR_MODE.store(mode);
|
||||
}
|
||||
SocketMessage::StackbarLabel(label) => {
|
||||
STACKBAR_LABEL.store(label);
|
||||
}
|
||||
SocketMessage::StackbarFocusedTextColour(r, g, b) => {
|
||||
let rgb = Rgb::new(r, g, b);
|
||||
STACKBAR_FOCUSED_TEXT_COLOUR.store(rgb.into(), Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::StackbarUnfocusedTextColour(r, g, b) => {
|
||||
let rgb = Rgb::new(r, g, b);
|
||||
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(rgb.into(), Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::StackbarBackgroundColour(r, g, b) => {
|
||||
let rgb = Rgb::new(r, g, b);
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.store(rgb.into(), Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::StackbarHeight(height) => {
|
||||
STACKBAR_TAB_HEIGHT.store(height, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::StackbarTabWidth(width) => {
|
||||
STACKBAR_TAB_WIDTH.store(width, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::StackbarFontSize(size) => {
|
||||
STACKBAR_FONT_SIZE.store(size, Ordering::SeqCst);
|
||||
}
|
||||
#[allow(clippy::assigning_clones)]
|
||||
SocketMessage::StackbarFontFamily(ref font_family) => {
|
||||
*STACKBAR_FONT_FAMILY.lock() = font_family.clone();
|
||||
}
|
||||
SocketMessage::ApplicationSpecificConfigurationSchema => {
|
||||
let asc = schema_for!(Vec<ApplicationConfiguration>);
|
||||
@@ -1294,134 +1421,31 @@ impl WindowManager {
|
||||
SocketMessage::ToggleTitleBars => {
|
||||
let current = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
REMOVE_TITLEBARS.store(!current, Ordering::SeqCst);
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::DebugWindow(hwnd) => {
|
||||
let window = Window::from(hwnd);
|
||||
let mut rule_debug = RuleDebug::default();
|
||||
let _ = window.should_manage(None, &mut rule_debug);
|
||||
let schema = serde_json::to_string_pretty(&rule_debug)?;
|
||||
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
// Deprecated commands
|
||||
SocketMessage::AltFocusHack(_)
|
||||
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
|
||||
};
|
||||
|
||||
match message {
|
||||
SocketMessage::ToggleMonocle => {
|
||||
let current = BORDER_COLOUR_CURRENT.load(Ordering::SeqCst);
|
||||
let monocle = BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst);
|
||||
|
||||
if monocle != 0 {
|
||||
if current == monocle {
|
||||
BORDER_COLOUR_CURRENT.store(
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
} else {
|
||||
BORDER_COLOUR_CURRENT.store(
|
||||
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::StackWindow(_) => {
|
||||
let stack = BORDER_COLOUR_STACK.load(Ordering::SeqCst);
|
||||
if stack != 0 {
|
||||
BORDER_COLOUR_CURRENT
|
||||
.store(BORDER_COLOUR_STACK.load(Ordering::SeqCst), Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
SocketMessage::UnstackWindow => {
|
||||
BORDER_COLOUR_CURRENT.store(
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match message {
|
||||
SocketMessage::ChangeLayout(_)
|
||||
| SocketMessage::CycleLayout(_)
|
||||
| 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::PromoteFocus
|
||||
| SocketMessage::StackWindow(_)
|
||||
| SocketMessage::UnstackWindow
|
||||
| SocketMessage::Retile
|
||||
// Adding this one so that changes can be seen instantly after
|
||||
// modifying the active window border offset
|
||||
| SocketMessage::ActiveWindowBorderOffset(_)
|
||||
// Adding this one because sometimes EVENT_SYSTEM_FOREGROUND isn't
|
||||
// getting sent on FocusWindow, meaning the border won't be set
|
||||
// when processing events
|
||||
| SocketMessage::FocusWindow(_)
|
||||
| SocketMessage::InvisibleBorders(_)
|
||||
| SocketMessage::WorkAreaOffset(_)
|
||||
| SocketMessage::CycleMoveWindow(_)
|
||||
| SocketMessage::MoveWindow(_)
|
||||
| SocketMessage::CycleFocusMonitor(_)
|
||||
| SocketMessage::CycleFocusWorkspace(_)
|
||||
| SocketMessage::FocusMonitorNumber(_)
|
||||
| SocketMessage::FocusMonitorWorkspaceNumber(_, _)
|
||||
| SocketMessage::FocusWorkspaceNumber(_) => {
|
||||
let foreground = WindowsApi::foreground_window()?;
|
||||
let foreground_window = Window { hwnd: foreground };
|
||||
|
||||
let monocle = BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst);
|
||||
if monocle != 0 && self.focused_workspace()?.monocle_container().is_some() {
|
||||
BORDER_COLOUR_CURRENT.store(
|
||||
monocle,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
|
||||
// it is not acceptable to fail here; we need to be able to send the event to
|
||||
// subscribers
|
||||
if self.focused_container().is_ok() {
|
||||
let stack = BORDER_COLOUR_STACK.load(Ordering::SeqCst);
|
||||
if stack != 0 && self.focused_container()?.windows().len() > 1 {
|
||||
BORDER_COLOUR_CURRENT
|
||||
.store(stack, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
border.set_position(foreground_window, 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, 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, true)?;
|
||||
focused.focus(false)?;
|
||||
} else {
|
||||
border.hide()?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
let notification = Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: self.as_ref().into(),
|
||||
};
|
||||
|
||||
notify_subscribers(&serde_json::to_string(¬ification)?)?;
|
||||
border_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
tracing::info!("processed");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1485,9 +1509,10 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream)
|
||||
|
||||
if wm.is_paused {
|
||||
return match message {
|
||||
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
|
||||
Ok(wm.process_command(message, &mut stream)?)
|
||||
}
|
||||
SocketMessage::TogglePause
|
||||
| SocketMessage::State
|
||||
| SocketMessage::GlobalState
|
||||
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
|
||||
_ => {
|
||||
tracing::trace!("ignoring while paused");
|
||||
Ok(())
|
||||
@@ -1496,10 +1521,6 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream)
|
||||
}
|
||||
|
||||
wm.process_command(message.clone(), &mut stream)?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: wm.as_ref().into(),
|
||||
})?)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1534,9 +1555,10 @@ pub fn read_commands_tcp(
|
||||
|
||||
if wm.is_paused {
|
||||
return match message {
|
||||
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
|
||||
Ok(wm.process_command(message, stream)?)
|
||||
}
|
||||
SocketMessage::TogglePause
|
||||
| SocketMessage::State
|
||||
| SocketMessage::GlobalState
|
||||
| SocketMessage::Stop => Ok(wm.process_command(message, stream)?),
|
||||
_ => {
|
||||
tracing::trace!("ignoring while paused");
|
||||
Ok(())
|
||||
@@ -1545,10 +1567,6 @@ pub fn read_commands_tcp(
|
||||
}
|
||||
|
||||
wm.process_command(message.clone(), &mut *stream)?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: wm.as_ref().into(),
|
||||
})?)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::select;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use komorebi_core::OperationDirection;
|
||||
@@ -12,24 +14,24 @@ use komorebi_core::Rect;
|
||||
use komorebi_core::Sizing;
|
||||
use komorebi_core::WindowContainerBehaviour;
|
||||
|
||||
use crate::border::Border;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::notify_subscribers;
|
||||
use crate::stackbar_manager;
|
||||
use crate::transparency_manager;
|
||||
use crate::window::should_act;
|
||||
use crate::window::RuleDebug;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::workspace_reconciliator;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::BORDER_COLOUR_CURRENT;
|
||||
use crate::BORDER_COLOUR_MONOCLE;
|
||||
use crate::BORDER_COLOUR_SINGLE;
|
||||
use crate::BORDER_COLOUR_STACK;
|
||||
use crate::BORDER_ENABLED;
|
||||
use crate::BORDER_HIDDEN;
|
||||
use crate::BORDER_HWND;
|
||||
use crate::BORDER_OFFSET;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::DATA_DIR;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
@@ -42,12 +44,15 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || {
|
||||
tracing::info!("listening");
|
||||
loop {
|
||||
select! {
|
||||
recv(receiver) -> mut maybe_event => {
|
||||
if let Ok(event) = maybe_event.as_mut() {
|
||||
match wm.lock().process_event(event) {
|
||||
Ok(()) => {},
|
||||
Err(error) => tracing::error!("{}", error)
|
||||
if let Ok(event) = receiver.recv() {
|
||||
let mut guard = wm.lock();
|
||||
match guard.process_event(event) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!("{:?}", error)
|
||||
} else {
|
||||
tracing::error!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,12 +64,47 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||
impl WindowManager {
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn process_event(&mut self, event: &mut WindowManagerEvent) -> Result<()> {
|
||||
pub fn process_event(&mut self, event: WindowManagerEvent) -> Result<()> {
|
||||
if self.is_paused {
|
||||
tracing::trace!("ignoring while paused");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut rule_debug = RuleDebug::default();
|
||||
|
||||
let should_manage = event.window().should_manage(Some(event), &mut rule_debug)?;
|
||||
|
||||
// All event handlers below this point should only be processed if the event is
|
||||
// related to a window that should be managed by the WindowManager.
|
||||
if !should_manage {
|
||||
let mut transparency_override = false;
|
||||
|
||||
if transparency_manager::TRANSPARENCY_ENABLED.load_consume() {
|
||||
for m in self.monitors() {
|
||||
for w in m.workspaces() {
|
||||
let event_hwnd = event.window().hwnd;
|
||||
|
||||
let visible_hwnds = w
|
||||
.visible_windows()
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if w.contains_managed_window(event_hwnd)
|
||||
&& !visible_hwnds.contains(&event_hwnd)
|
||||
{
|
||||
transparency_override = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !transparency_override {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
|
||||
if let Some(id) = current_virtual_desktop() {
|
||||
if id != *virtual_desktop_id {
|
||||
@@ -81,57 +121,38 @@ impl WindowManager {
|
||||
match event {
|
||||
WindowManagerEvent::FocusChange(_, window)
|
||||
| WindowManagerEvent::Show(_, window)
|
||||
| WindowManagerEvent::DisplayChange(window)
|
||||
| WindowManagerEvent::MoveResizeEnd(_, window) => {
|
||||
self.reconcile_monitors()?;
|
||||
|
||||
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.focused_monitor_idx() != monitor_idx
|
||||
{
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_window(window) {
|
||||
// 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 let Ok(class) = window.class() {
|
||||
if class != "OleMainThreadWndClass"
|
||||
&& self.focused_monitor_idx() != monitor_idx
|
||||
{
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
for (i, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
||||
let work_area = *monitor.work_area_size();
|
||||
let offset = if monitor.work_area_offset().is_some() {
|
||||
monitor.work_area_offset()
|
||||
} else {
|
||||
offset
|
||||
};
|
||||
|
||||
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||
let reaped_orphans = workspace.reap_orphans()?;
|
||||
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
|
||||
workspace.update(&work_area, offset)?;
|
||||
tracing::info!(
|
||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||
reaped_orphans.0,
|
||||
reaped_orphans.1,
|
||||
i,
|
||||
j
|
||||
);
|
||||
for monitor in self.monitors_mut() {
|
||||
for workspace in monitor.workspaces_mut() {
|
||||
if let WindowManagerEvent::FocusChange(_, window) = event {
|
||||
let _ = workspace.focus_changed(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,12 +172,14 @@ impl WindowManager {
|
||||
self.has_pending_raise_op = false;
|
||||
}
|
||||
WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => {
|
||||
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||
self.update_focused_workspace(false)?;
|
||||
if self.focused_workspace()?.contains_window(window.hwnd) {
|
||||
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
|
||||
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
|
||||
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
|
||||
|
||||
already_moved_window_handles.remove(&window.hwnd);
|
||||
already_moved_window_handles.remove(&window.hwnd);
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::Minimize(_, window) => {
|
||||
let mut hide = false;
|
||||
@@ -170,7 +193,7 @@ impl WindowManager {
|
||||
|
||||
if hide {
|
||||
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::Hide(_, window) => {
|
||||
@@ -199,7 +222,8 @@ impl WindowManager {
|
||||
path,
|
||||
&tray_and_multi_window_identifiers,
|
||||
®ex_identifiers,
|
||||
);
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if !window.is_window()
|
||||
|| should_act
|
||||
@@ -211,7 +235,7 @@ impl WindowManager {
|
||||
|
||||
if hide {
|
||||
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
|
||||
let mut already_moved_window_handles = self.already_moved_window_handles.lock();
|
||||
@@ -219,6 +243,8 @@ impl WindowManager {
|
||||
already_moved_window_handles.remove(&window.hwnd);
|
||||
}
|
||||
WindowManagerEvent::FocusChange(_, window) => {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if !workspace
|
||||
.floating_windows()
|
||||
@@ -241,27 +267,41 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::Show(_, window) | WindowManagerEvent::Manage(window) => {
|
||||
let mut switch_to = None;
|
||||
WindowManagerEvent::Show(_, window)
|
||||
| WindowManagerEvent::Manage(window)
|
||||
| WindowManagerEvent::Uncloak(_, window) => {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
let mut needs_reconciliation = false;
|
||||
|
||||
for (i, monitors) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitors.workspaces().iter().enumerate() {
|
||||
if workspace.contains_window(window.hwnd) {
|
||||
switch_to = Some((i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
|
||||
// At this point we know we are going to send a notification to the workspace reconciliator
|
||||
// So we get the topmost window returned by EnumWindows, which is almost always the window
|
||||
// that has been selected by alt-tab
|
||||
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
|
||||
if let Some(first) =
|
||||
alt_tab_windows.iter().find(|w| w.title().is_ok())
|
||||
{
|
||||
// If our record of this HWND hasn't been updated in over a minute
|
||||
let mut instant = ALT_TAB_HWND_INSTANT.lock();
|
||||
if instant.elapsed().gt(&Duration::from_secs(1)) {
|
||||
// Update our record with the HWND we just found
|
||||
ALT_TAB_HWND.store(Some(first.hwnd));
|
||||
// Update the timestamp of our record
|
||||
*instant = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((known_monitor_idx, known_workspace_idx)) = switch_to {
|
||||
if self.focused_monitor_idx() != known_monitor_idx
|
||||
|| self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?
|
||||
.focused_workspace_idx()
|
||||
!= known_workspace_idx
|
||||
{
|
||||
self.focus_monitor(known_monitor_idx)?;
|
||||
self.focus_workspace(known_workspace_idx)?;
|
||||
return Ok(());
|
||||
workspace_reconciliator::send_notification(i, j);
|
||||
needs_reconciliation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +310,8 @@ impl WindowManager {
|
||||
// result in them being associated with both the original workspace and the workspace
|
||||
// being switched to. This loop is to try to ensure that we don't end up with
|
||||
// duplicates across multiple workspaces, as it results in ghost layout tiles.
|
||||
let mut proceed = true;
|
||||
|
||||
for (i, monitor) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
if workspace.container_for_window(window.hwnd).is_some()
|
||||
@@ -281,26 +323,48 @@ impl WindowManager {
|
||||
);
|
||||
|
||||
window.hide();
|
||||
return Ok(());
|
||||
proceed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let behaviour = self.window_container_behaviour;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if proceed {
|
||||
let behaviour =
|
||||
self.window_container_behaviour(focused_monitor_idx, focused_workspace_idx);
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let workspace_contains_window = workspace.contains_window(window.hwnd);
|
||||
let monocle_container = workspace.monocle_container().clone();
|
||||
|
||||
if !workspace.contains_window(window.hwnd) {
|
||||
match behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
workspace.new_container_for_window(*window);
|
||||
self.update_focused_workspace(false)?;
|
||||
if !workspace_contains_window && !needs_reconciliation {
|
||||
match behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
workspace.new_container_for_window(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
workspace
|
||||
.focused_container_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused container"))?
|
||||
.add_window(window);
|
||||
self.update_focused_workspace(true, false)?;
|
||||
|
||||
stackbar_manager::send_notification();
|
||||
}
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
workspace
|
||||
.focused_container_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused container"))?
|
||||
.add_window(*window);
|
||||
self.update_focused_workspace(true)?;
|
||||
}
|
||||
|
||||
if workspace_contains_window {
|
||||
let mut monocle_window_event = false;
|
||||
if let Some(ref monocle) = monocle_container {
|
||||
if let Some(monocle_window) = monocle.focused_window() {
|
||||
if monocle_window.hwnd == window.hwnd {
|
||||
monocle_window_event = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !monocle_window_event && monocle_container.is_some() {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,10 +400,12 @@ impl WindowManager {
|
||||
.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 focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
|
||||
let window_container_behaviour =
|
||||
self.window_container_behaviour(focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
let new_position = WindowsApi::window_rect(window.hwnd())?;
|
||||
let old_position = *workspace
|
||||
@@ -395,10 +461,7 @@ impl WindowManager {
|
||||
|
||||
// If we have moved across the monitors, use that override, otherwise determine
|
||||
// if a move has taken place by ruling out a resize
|
||||
let right_bottom_constant = ((BORDER_WIDTH.load(Ordering::SeqCst)
|
||||
+ BORDER_OFFSET.load(Ordering::SeqCst))
|
||||
* 2)
|
||||
.abs();
|
||||
let right_bottom_constant = 0;
|
||||
|
||||
let is_move = moved_across_monitors
|
||||
|| resize.right.abs() == right_bottom_constant
|
||||
@@ -450,26 +513,27 @@ impl WindowManager {
|
||||
// the origin monitor's focused workspace
|
||||
self.focus_monitor(origin_monitor_idx)?;
|
||||
self.focus_workspace(origin_workspace_idx)?;
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
|
||||
self.focus_monitor(target_monitor_idx)?;
|
||||
self.focus_workspace(target_workspace_idx)?;
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
// Here we handle a simple move on the same monitor which is treated as
|
||||
// a container swap
|
||||
} else {
|
||||
match new_window_behaviour {
|
||||
match window_container_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)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
None => {
|
||||
self.update_focused_workspace(
|
||||
self.mouse_follows_focus,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -478,14 +542,17 @@ impl WindowManager {
|
||||
match workspace.container_idx_from_current_point() {
|
||||
Some(target_idx) => {
|
||||
workspace.move_window_to_container(target_idx)?;
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
None => {
|
||||
self.update_focused_workspace(
|
||||
self.mouse_follows_focus,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
stackbar_manager::send_notification();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,119 +596,20 @@ impl WindowManager {
|
||||
self.resize_window(edge, sizing, delta, true)?;
|
||||
}
|
||||
|
||||
self.update_focused_workspace(false)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::ForceUpdate(_) => {
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
WindowManagerEvent::DisplayChange(..)
|
||||
| WindowManagerEvent::MouseCapture(..)
|
||||
WindowManagerEvent::MouseCapture(..)
|
||||
| WindowManagerEvent::Cloak(..)
|
||||
| WindowManagerEvent::Uncloak(..) => {}
|
||||
| WindowManagerEvent::TitleUpdate(..) => {}
|
||||
};
|
||||
|
||||
if !self.focused_workspace()?.tile() {
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
border.hide()?;
|
||||
BORDER_HIDDEN.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
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(_, window)
|
||||
| WindowManagerEvent::Show(_, window)
|
||||
| WindowManagerEvent::FocusChange(_, window)
|
||||
| WindowManagerEvent::Hide(_, window)
|
||||
| WindowManagerEvent::Uncloak(_, window)
|
||||
| WindowManagerEvent::Minimize(_, window) => {
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
let mut target_window = None;
|
||||
let mut target_window_is_monocle = false;
|
||||
if self
|
||||
.focused_workspace()?
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.any(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
target_window = Option::from(*window);
|
||||
WindowsApi::raise_window(border.hwnd())?;
|
||||
};
|
||||
|
||||
if let Some(monocle_container) = self.focused_workspace()?.monocle_container() {
|
||||
if let Some(window) = monocle_container.focused_window() {
|
||||
target_window = Option::from(*window);
|
||||
target_window_is_monocle = true;
|
||||
}
|
||||
}
|
||||
|
||||
if target_window.is_none() {
|
||||
match self.focused_container() {
|
||||
// if there is no focused container, the desktop is empty
|
||||
Err(..) => {
|
||||
WindowsApi::hide_border_window(border.hwnd())?;
|
||||
}
|
||||
Ok(container) => {
|
||||
if !(matches!(event, WindowManagerEvent::Minimize(_, _))
|
||||
&& container.windows().len() == 1)
|
||||
{
|
||||
let container_size = self.focused_container()?.windows().len();
|
||||
target_window = Option::from(*self.focused_window()?);
|
||||
|
||||
if target_window_is_monocle {
|
||||
BORDER_COLOUR_CURRENT.store(
|
||||
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
} else if container_size > 1 {
|
||||
BORDER_COLOUR_CURRENT.store(
|
||||
BORDER_COLOUR_STACK.load(Ordering::SeqCst),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
} else {
|
||||
BORDER_COLOUR_CURRENT.store(
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(target_window) = target_window {
|
||||
let activate = BORDER_HIDDEN.load(Ordering::SeqCst);
|
||||
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
border.set_position(target_window, activate)?;
|
||||
|
||||
if activate {
|
||||
BORDER_HIDDEN.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
|
||||
if let WindowManagerEvent::Unmanage(window) = event {
|
||||
if let WindowManagerEvent::Unmanage(mut window) = event {
|
||||
window.center(&self.focused_monitor_work_area()?)?;
|
||||
}
|
||||
|
||||
// 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");
|
||||
let mut known_hwnds = vec![];
|
||||
for monitor in self.monitors() {
|
||||
@@ -662,12 +630,27 @@ impl WindowManager {
|
||||
.open(hwnd_json)?;
|
||||
|
||||
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());
|
||||
let notification = Notification {
|
||||
event: NotificationEvent::WindowManager(event),
|
||||
state: self.as_ref().into(),
|
||||
};
|
||||
|
||||
notify_subscribers(&serde_json::to_string(¬ification)?)?;
|
||||
border_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
// Too many spammy OBJECT_NAMECHANGE events from JetBrains IDEs
|
||||
if !matches!(
|
||||
event,
|
||||
WindowManagerEvent::Show(WinEvent::ObjectNameChange, _)
|
||||
) {
|
||||
tracing::info!("processed: {}", event.window().to_string());
|
||||
} else {
|
||||
tracing::trace!("processed: {}", event.window().to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
66
komorebi/src/reaper.rs
Normal file
66
komorebi/src/reaper.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::WindowManager;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn watch_for_orphans(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match find_orphans(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!("restarting failed thread: {:?}", error)
|
||||
} else {
|
||||
tracing::error!("restarting failed thread: {}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("watching");
|
||||
|
||||
let arc = wm.clone();
|
||||
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
let mut wm = arc.lock();
|
||||
let offset = wm.work_area_offset;
|
||||
|
||||
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
|
||||
let work_area = *monitor.work_area_size();
|
||||
let window_based_work_area_offset = (
|
||||
monitor.window_based_work_area_offset_limit(),
|
||||
monitor.window_based_work_area_offset(),
|
||||
);
|
||||
|
||||
let offset = if monitor.work_area_offset().is_some() {
|
||||
monitor.work_area_offset()
|
||||
} else {
|
||||
offset
|
||||
};
|
||||
|
||||
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||
let reaped_orphans = workspace.reap_orphans()?;
|
||||
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
|
||||
workspace.update(&work_area, offset, window_based_work_area_offset)?;
|
||||
border_manager::send_notification();
|
||||
tracing::info!(
|
||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||
reaped_orphans.0,
|
||||
reaped_orphans.1,
|
||||
i,
|
||||
j
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Ring<T> {
|
||||
elements: VecDeque<T>,
|
||||
focused: usize,
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Gdi::CreateFontIndirectW;
|
||||
use windows::Win32::Graphics::Gdi::CreatePen;
|
||||
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
|
||||
use windows::Win32::Graphics::Gdi::DrawTextW;
|
||||
use windows::Win32::Graphics::Gdi::GetDC;
|
||||
use windows::Win32::Graphics::Gdi::ReleaseDC;
|
||||
use windows::Win32::Graphics::Gdi::SelectObject;
|
||||
use windows::Win32::Graphics::Gdi::SetBkColor;
|
||||
use windows::Win32::Graphics::Gdi::SetTextColor;
|
||||
use windows::Win32::Graphics::Gdi::DT_CENTER;
|
||||
use windows::Win32::Graphics::Gdi::DT_END_ELLIPSIS;
|
||||
use windows::Win32::Graphics::Gdi::DT_SINGLELINE;
|
||||
use windows::Win32::Graphics::Gdi::DT_VCENTER;
|
||||
use windows::Win32::Graphics::Gdi::FONT_QUALITY;
|
||||
use windows::Win32::Graphics::Gdi::FW_BOLD;
|
||||
use windows::Win32::Graphics::Gdi::LOGFONTW;
|
||||
use windows::Win32::Graphics::Gdi::PROOF_QUALITY;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::RegisterClassW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SW_SHOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
|
||||
|
||||
use komorebi_core::Rect;
|
||||
|
||||
use crate::window::Window;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::winevent_listener;
|
||||
use crate::WindowManagerEvent;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::STACKBAR_TAB_HEIGHT;
|
||||
use crate::STACKBAR_TAB_WIDTH;
|
||||
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::TRANSPARENCY_COLOUR;
|
||||
use crate::WINDOWS_BY_BAR_HWNDS;
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Stackbar {
|
||||
pub(crate) hwnd: isize,
|
||||
#[serde(skip)]
|
||||
pub is_cloned: bool,
|
||||
}
|
||||
|
||||
impl Drop for Stackbar {
|
||||
fn drop(&mut self) {
|
||||
if !self.is_cloned {
|
||||
let _ = WindowsApi::close_window(self.hwnd());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Stackbar {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
hwnd: self.hwnd,
|
||||
is_cloned: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Stackbar {
|
||||
unsafe extern "system" fn window_proc(
|
||||
hwnd: HWND,
|
||||
msg: u32,
|
||||
w_param: WPARAM,
|
||||
l_param: LPARAM,
|
||||
) -> LRESULT {
|
||||
match msg {
|
||||
WM_LBUTTONDOWN => {
|
||||
let win_hwnds_by_topbar = WINDOWS_BY_BAR_HWNDS.lock();
|
||||
if let Some(win_hwnds) = win_hwnds_by_topbar.get(&hwnd.0) {
|
||||
let x = l_param.0 as i32 & 0xFFFF;
|
||||
let y = (l_param.0 as i32 >> 16) & 0xFFFF;
|
||||
|
||||
let width = STACKBAR_TAB_WIDTH.load(Ordering::SeqCst);
|
||||
let height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
|
||||
let gap = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
|
||||
|
||||
for (index, win_hwnd) in win_hwnds.iter().enumerate() {
|
||||
let left = gap + (index as i32 * (width + gap));
|
||||
let right = left + width;
|
||||
let top = 0;
|
||||
let bottom = height;
|
||||
|
||||
if x >= left && x <= right && y >= top && y <= bottom {
|
||||
let window = Window { hwnd: *win_hwnd };
|
||||
let event_sender = winevent_listener::event_tx();
|
||||
let _ = event_sender.send(WindowManagerEvent::FocusChange(
|
||||
WinEvent::ObjectFocus,
|
||||
window,
|
||||
));
|
||||
let _ = event_sender.send(WindowManagerEvent::ForceUpdate(window));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WINDOWS_BY_BAR_HWNDS.force_unlock();
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(hwnd, msg, w_param, l_param),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn hwnd(&self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn create() -> Result<Stackbar> {
|
||||
let name: Vec<u16> = "komorebi_stackbar\0".encode_utf16().collect();
|
||||
let class_name = PCWSTR(name.as_ptr());
|
||||
|
||||
let h_module = WindowsApi::module_handle_w()?;
|
||||
|
||||
let wnd_class = WNDCLASSW {
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(Self::window_proc),
|
||||
hInstance: h_module.into(),
|
||||
lpszClassName: class_name,
|
||||
hbrBackground: WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
unsafe {
|
||||
RegisterClassW(&wnd_class);
|
||||
}
|
||||
|
||||
let (hwnd_sender, hwnd_receiver) = crossbeam_channel::bounded::<HWND>(1);
|
||||
|
||||
let name_cl = name.clone();
|
||||
std::thread::spawn(move || -> Result<()> {
|
||||
unsafe {
|
||||
let hwnd = CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED,
|
||||
PCWSTR(name_cl.as_ptr()),
|
||||
PCWSTR(name_cl.as_ptr()),
|
||||
WS_POPUP | WS_VISIBLE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
h_module,
|
||||
None,
|
||||
);
|
||||
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
|
||||
let mut msg = MSG::default();
|
||||
while GetMessageW(&mut msg, hwnd, 0, 0).into() {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
hwnd: hwnd_receiver.recv()?.0,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
|
||||
WindowsApi::position_window(self.hwnd(), layout, top)
|
||||
}
|
||||
|
||||
pub fn get_position_from_container_layout(&self, layout: &Rect) -> Rect {
|
||||
Rect {
|
||||
bottom: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
|
||||
..*layout
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&self, windows: &VecDeque<Window>, focused_hwnd: isize) -> Result<()> {
|
||||
let width = STACKBAR_TAB_WIDTH.load(Ordering::SeqCst);
|
||||
let height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
|
||||
let gap = DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst);
|
||||
let background = STACKBAR_TAB_BACKGROUND_COLOUR.load(Ordering::SeqCst);
|
||||
let focused_text_colour = STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst);
|
||||
let unfocused_text_colour = STACKBAR_UNFOCUSED_TEXT_COLOUR.load(Ordering::SeqCst);
|
||||
|
||||
unsafe {
|
||||
let hdc = GetDC(self.hwnd());
|
||||
|
||||
let hpen = CreatePen(PS_SOLID, 0, COLORREF(background));
|
||||
let hbrush = CreateSolidBrush(COLORREF(background));
|
||||
|
||||
SelectObject(hdc, hpen);
|
||||
SelectObject(hdc, hbrush);
|
||||
SetBkColor(hdc, COLORREF(background));
|
||||
|
||||
let hfont = CreateFontIndirectW(&LOGFONTW {
|
||||
lfWeight: FW_BOLD.0 as i32,
|
||||
lfQuality: FONT_QUALITY(PROOF_QUALITY.0),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
SelectObject(hdc, hfont);
|
||||
|
||||
for (i, window) in windows.iter().enumerate() {
|
||||
if window.hwnd == focused_hwnd {
|
||||
SetTextColor(hdc, COLORREF(focused_text_colour));
|
||||
|
||||
window.focus(false)?;
|
||||
} else {
|
||||
SetTextColor(hdc, COLORREF(unfocused_text_colour));
|
||||
}
|
||||
|
||||
let left = gap + (i as i32 * (width + gap));
|
||||
let mut tab_box = Rect {
|
||||
top: 0,
|
||||
left,
|
||||
right: left + width,
|
||||
bottom: height,
|
||||
};
|
||||
|
||||
WindowsApi::round_rect(hdc, &tab_box, 8);
|
||||
|
||||
let exe = window.exe()?;
|
||||
let exe_trimmed = exe.trim_end_matches(".exe");
|
||||
let mut tab_title: Vec<u16> = exe_trimmed.encode_utf16().collect();
|
||||
|
||||
tab_box.left_padding(10);
|
||||
tab_box.right_padding(10);
|
||||
|
||||
DrawTextW(
|
||||
hdc,
|
||||
&mut tab_title,
|
||||
&mut tab_box.into(),
|
||||
DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS,
|
||||
);
|
||||
}
|
||||
|
||||
ReleaseDC(self.hwnd(), hdc);
|
||||
}
|
||||
|
||||
let mut windows_hwdns: VecDeque<isize> = VecDeque::new();
|
||||
for window in windows {
|
||||
windows_hwdns.push_back(window.hwnd);
|
||||
}
|
||||
|
||||
WINDOWS_BY_BAR_HWNDS.lock().insert(self.hwnd, windows_hwdns);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide(&self) {
|
||||
WindowsApi::hide_window(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn restore(&self) {
|
||||
WindowsApi::show_window(self.hwnd(), SW_SHOW)
|
||||
}
|
||||
}
|
||||
224
komorebi/src/stackbar_manager/mod.rs
Normal file
224
komorebi/src/stackbar_manager/mod.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
mod stackbar;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::stackbar_manager::stackbar::Stackbar;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::StackbarLabel;
|
||||
use komorebi_core::StackbarMode;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
pub static STACKBAR_FONT_SIZE: AtomicI32 = AtomicI32::new(0); // 0 will produce the system default
|
||||
pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white
|
||||
pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947); // gray text
|
||||
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
|
||||
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
|
||||
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
|
||||
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
|
||||
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::OnStack);
|
||||
|
||||
pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref STACKBAR_STATE: Mutex<HashMap<String, Stackbar>> = Mutex::new(HashMap::new());
|
||||
pub static ref STACKBAR_FONT_FAMILY: Mutex<Option<String>> = Mutex::new(None);
|
||||
static ref STACKBARS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
|
||||
static ref STACKBARS_CONTAINERS: Mutex<HashMap<isize, Container>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub struct Notification;
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification() {
|
||||
if event_tx().try_send(Notification).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_have_stackbar(window_count: usize) -> bool {
|
||||
match STACKBAR_MODE.load() {
|
||||
StackbarMode::Always => true,
|
||||
StackbarMode::OnStack => window_count > 1,
|
||||
StackbarMode::Never => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
|
||||
'receiver: for _ in receiver {
|
||||
let mut stackbars = STACKBAR_STATE.lock();
|
||||
let mut stackbars_monitors = STACKBARS_MONITORS.lock();
|
||||
|
||||
// Check the wm state every time we receive a notification
|
||||
let mut state = wm.lock();
|
||||
|
||||
// If stackbars are disabled
|
||||
if matches!(STACKBAR_MODE.load(), StackbarMode::Never)
|
||||
|| STACKBAR_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
|
||||
{
|
||||
for (_, stackbar) in stackbars.iter() {
|
||||
stackbar.destroy()?;
|
||||
}
|
||||
|
||||
stackbars.clear();
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
for (monitor_idx, m) in state.monitors_mut().iter_mut().enumerate() {
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace_mut() {
|
||||
// Workspaces with tiling disabled don't have stackbars
|
||||
if !ws.tile() {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in stackbars.iter() {
|
||||
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
stackbars.remove(id);
|
||||
}
|
||||
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
let is_maximized = WindowsApi::is_zoomed(HWND(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
));
|
||||
|
||||
// Handle the monocle container separately
|
||||
if ws.monocle_container().is_some() || is_maximized {
|
||||
// Destroy any stackbars associated with the focused workspace
|
||||
let mut to_remove = vec![];
|
||||
for (id, stackbar) in stackbars.iter() {
|
||||
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
|
||||
stackbar.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
stackbars.remove(id);
|
||||
}
|
||||
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
// Destroy any stackbars not associated with the focused workspace
|
||||
let container_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
.map(|c| c.id().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut to_remove = vec![];
|
||||
for (id, stackbar) in stackbars.iter() {
|
||||
if stackbars_monitors.get(id).copied().unwrap_or_default() == monitor_idx
|
||||
&& !container_ids.contains(id)
|
||||
{
|
||||
stackbar.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
stackbars.remove(id);
|
||||
}
|
||||
|
||||
let container_padding = ws
|
||||
.container_padding()
|
||||
.unwrap_or_else(|| DEFAULT_CONTAINER_PADDING.load_consume());
|
||||
|
||||
'containers: for container in ws.containers_mut() {
|
||||
let should_add_stackbar = match STACKBAR_MODE.load() {
|
||||
StackbarMode::Always => true,
|
||||
StackbarMode::OnStack => container.windows().len() > 1,
|
||||
StackbarMode::Never => false,
|
||||
};
|
||||
|
||||
if !should_add_stackbar {
|
||||
if let Some(stackbar) = stackbars.get(container.id()) {
|
||||
stackbar.destroy()?
|
||||
}
|
||||
|
||||
stackbars.remove(container.id());
|
||||
stackbars_monitors.remove(container.id());
|
||||
continue 'containers;
|
||||
}
|
||||
|
||||
// Get the stackbar entry for this container from the map or create one
|
||||
let stackbar = match stackbars.entry(container.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(stackbar) = Stackbar::create(container.id()) {
|
||||
entry.insert(stackbar)
|
||||
} else {
|
||||
continue 'receiver;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
stackbars_monitors.insert(container.id().clone(), monitor_idx);
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
container
|
||||
.focused_window()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.hwnd(),
|
||||
)?;
|
||||
|
||||
stackbar.update(container_padding, container, &rect)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
370
komorebi/src/stackbar_manager/stackbar.rs
Normal file
370
komorebi/src/stackbar_manager/stackbar.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::container::Container;
|
||||
use crate::stackbar_manager::STACKBARS_CONTAINERS;
|
||||
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
|
||||
use crate::stackbar_manager::STACKBAR_LABEL;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
|
||||
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::WindowsApi;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::WINDOWS_11;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::BorderStyle;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::StackbarLabel;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Gdi::CreateFontIndirectW;
|
||||
use windows::Win32::Graphics::Gdi::CreatePen;
|
||||
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
|
||||
use windows::Win32::Graphics::Gdi::DeleteObject;
|
||||
use windows::Win32::Graphics::Gdi::DrawTextW;
|
||||
use windows::Win32::Graphics::Gdi::GetDC;
|
||||
use windows::Win32::Graphics::Gdi::GetDeviceCaps;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::ReleaseDC;
|
||||
use windows::Win32::Graphics::Gdi::RoundRect;
|
||||
use windows::Win32::Graphics::Gdi::SelectObject;
|
||||
use windows::Win32::Graphics::Gdi::SetBkColor;
|
||||
use windows::Win32::Graphics::Gdi::SetTextColor;
|
||||
use windows::Win32::Graphics::Gdi::DT_CENTER;
|
||||
use windows::Win32::Graphics::Gdi::DT_END_ELLIPSIS;
|
||||
use windows::Win32::Graphics::Gdi::DT_SINGLELINE;
|
||||
use windows::Win32::Graphics::Gdi::DT_VCENTER;
|
||||
use windows::Win32::Graphics::Gdi::FONT_QUALITY;
|
||||
use windows::Win32::Graphics::Gdi::FW_BOLD;
|
||||
use windows::Win32::Graphics::Gdi::LOGFONTW;
|
||||
use windows::Win32::Graphics::Gdi::LOGPIXELSY;
|
||||
use windows::Win32::Graphics::Gdi::PROOF_QUALITY;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
use windows::Win32::System::WindowsProgramming::MulDiv;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_VISIBLE;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Stackbar {
|
||||
pub hwnd: isize,
|
||||
}
|
||||
|
||||
impl From<isize> for Stackbar {
|
||||
fn from(value: isize) -> Self {
|
||||
Self { hwnd: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Stackbar {
|
||||
pub const fn hwnd(&self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn create(id: &str) -> color_eyre::Result<Self> {
|
||||
let name: Vec<u16> = format!("komostackbar-{id}\0").encode_utf16().collect();
|
||||
let class_name = PCWSTR(name.as_ptr());
|
||||
|
||||
let h_module = WindowsApi::module_handle_w()?;
|
||||
|
||||
let window_class = WNDCLASSW {
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(Self::callback),
|
||||
hInstance: h_module.into(),
|
||||
lpszClassName: class_name,
|
||||
hbrBackground: WindowsApi::create_solid_brush(0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _ = WindowsApi::register_class_w(&window_class);
|
||||
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
|
||||
let name_cl = name.clone();
|
||||
std::thread::spawn(move || -> color_eyre::Result<()> {
|
||||
unsafe {
|
||||
let hwnd = CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED,
|
||||
PCWSTR(name_cl.as_ptr()),
|
||||
PCWSTR(name_cl.as_ptr()),
|
||||
WS_POPUP | WS_VISIBLE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
h_module,
|
||||
None,
|
||||
);
|
||||
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
|
||||
let mut msg: MSG = MSG::default();
|
||||
|
||||
loop {
|
||||
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
|
||||
tracing::debug!("stackbar window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
|
||||
std::thread::sleep(Duration::from_millis(10))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
hwnd: hwnd_receiver.recv()?.0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
WindowsApi::close_window(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&self,
|
||||
container_padding: i32,
|
||||
container: &mut Container,
|
||||
layout: &Rect,
|
||||
) -> color_eyre::Result<()> {
|
||||
let width = STACKBAR_TAB_WIDTH.load_consume();
|
||||
let height = STACKBAR_TAB_HEIGHT.load_consume();
|
||||
let gap = DEFAULT_CONTAINER_PADDING.load_consume();
|
||||
let background = STACKBAR_TAB_BACKGROUND_COLOUR.load_consume();
|
||||
let focused_text_colour = STACKBAR_FOCUSED_TEXT_COLOUR.load_consume();
|
||||
let unfocused_text_colour = STACKBAR_UNFOCUSED_TEXT_COLOUR.load_consume();
|
||||
|
||||
let mut stackbars_containers = STACKBARS_CONTAINERS.lock();
|
||||
stackbars_containers.insert(self.hwnd, container.clone());
|
||||
|
||||
let mut layout = *layout;
|
||||
let workspace_specific_offset =
|
||||
BORDER_WIDTH.load_consume() + BORDER_OFFSET.load_consume() + container_padding;
|
||||
|
||||
layout.top -= workspace_specific_offset + STACKBAR_TAB_HEIGHT.load_consume();
|
||||
layout.left -= workspace_specific_offset;
|
||||
|
||||
WindowsApi::position_window(self.hwnd(), &layout, false)?;
|
||||
|
||||
unsafe {
|
||||
let hdc = GetDC(self.hwnd());
|
||||
|
||||
let hpen = CreatePen(PS_SOLID, 0, COLORREF(background));
|
||||
let hbrush = CreateSolidBrush(COLORREF(background));
|
||||
|
||||
SelectObject(hdc, hpen);
|
||||
SelectObject(hdc, hbrush);
|
||||
SetBkColor(hdc, COLORREF(background));
|
||||
|
||||
let mut logfont = LOGFONTW {
|
||||
lfWeight: FW_BOLD.0 as i32,
|
||||
lfQuality: FONT_QUALITY(PROOF_QUALITY.0),
|
||||
lfFaceName: [0; 32],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(font_name) = &*STACKBAR_FONT_FAMILY.lock() {
|
||||
let font = wide_string(font_name);
|
||||
for (i, &c) in font.iter().enumerate() {
|
||||
logfont.lfFaceName[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
let logical_height = -MulDiv(
|
||||
STACKBAR_FONT_SIZE.load(Ordering::SeqCst),
|
||||
72,
|
||||
GetDeviceCaps(hdc, LOGPIXELSY),
|
||||
);
|
||||
|
||||
logfont.lfHeight = logical_height;
|
||||
|
||||
let hfont = CreateFontIndirectW(&logfont);
|
||||
|
||||
SelectObject(hdc, hfont);
|
||||
|
||||
for (i, window) in container.windows().iter().enumerate() {
|
||||
if window.hwnd == container.focused_window().copied().unwrap_or_default().hwnd {
|
||||
SetTextColor(hdc, COLORREF(focused_text_colour));
|
||||
} else {
|
||||
SetTextColor(hdc, COLORREF(unfocused_text_colour));
|
||||
}
|
||||
|
||||
let left = gap + (i as i32 * (width + gap));
|
||||
let mut rect = Rect {
|
||||
top: 0,
|
||||
left,
|
||||
right: left + width,
|
||||
bottom: height,
|
||||
};
|
||||
|
||||
match STYLE.load() {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
|
||||
} else {
|
||||
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => {
|
||||
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
|
||||
let label = match STACKBAR_LABEL.load() {
|
||||
StackbarLabel::Process => {
|
||||
let exe = window.exe()?;
|
||||
exe.trim_end_matches(".exe").to_string()
|
||||
}
|
||||
StackbarLabel::Title => window.title()?,
|
||||
};
|
||||
|
||||
let mut tab_title: Vec<u16> = label.encode_utf16().collect();
|
||||
|
||||
rect.left_padding(10);
|
||||
rect.right_padding(10);
|
||||
|
||||
DrawTextW(
|
||||
hdc,
|
||||
&mut tab_title,
|
||||
&mut rect.into(),
|
||||
DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_END_ELLIPSIS,
|
||||
);
|
||||
}
|
||||
|
||||
ReleaseDC(self.hwnd(), hdc);
|
||||
DeleteObject(hpen);
|
||||
DeleteObject(hbrush);
|
||||
DeleteObject(hfont);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_position_from_container_layout(&self, layout: &Rect) -> Rect {
|
||||
Rect {
|
||||
bottom: STACKBAR_TAB_HEIGHT.load_consume(),
|
||||
..*layout
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "system" fn callback(
|
||||
hwnd: HWND,
|
||||
msg: u32,
|
||||
w_param: WPARAM,
|
||||
l_param: LPARAM,
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match msg {
|
||||
WM_LBUTTONDOWN => {
|
||||
let stackbars_containers = STACKBARS_CONTAINERS.lock();
|
||||
if let Some(container) = stackbars_containers.get(&hwnd.0) {
|
||||
let x = l_param.0 as i32 & 0xFFFF;
|
||||
let y = (l_param.0 as i32 >> 16) & 0xFFFF;
|
||||
|
||||
let width = STACKBAR_TAB_WIDTH.load_consume();
|
||||
let height = STACKBAR_TAB_HEIGHT.load_consume();
|
||||
let gap = DEFAULT_CONTAINER_PADDING.load_consume();
|
||||
|
||||
let focused_window_idx = container.focused_window_idx();
|
||||
let focused_window_rect = WindowsApi::window_rect(
|
||||
container
|
||||
.focused_window()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.hwnd(),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
for (index, window) in container.windows().iter().enumerate() {
|
||||
let left = gap + (index as i32 * (width + gap));
|
||||
let right = left + width;
|
||||
let top = 0;
|
||||
let bottom = height;
|
||||
|
||||
if x >= left && x <= right && y >= top && y <= bottom {
|
||||
// If we are focusing a window that isn't currently focused in the
|
||||
// stackbar, make sure we update its location so that it doesn't render
|
||||
// on top of other tiles before eventually ending up in the correct
|
||||
// tile
|
||||
if index != focused_window_idx {
|
||||
if let Err(err) =
|
||||
window.set_position(&focused_window_rect, false)
|
||||
{
|
||||
tracing::error!(
|
||||
"stackbar WM_LBUTTONDOWN repositioning error: hwnd {} ({})",
|
||||
*window,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the window corresponding to the tab we have clicked
|
||||
window.restore();
|
||||
if let Err(err) = window.focus(false) {
|
||||
tracing::error!(
|
||||
"stackbar WMLBUTTONDOWN focus error: hwnd {} ({})",
|
||||
*window,
|
||||
err
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Hide any windows in the stack that don't correspond to the window
|
||||
// we have clicked
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(hwnd, msg, w_param, l_param),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wide_string(s: &str) -> Vec<u16> {
|
||||
std::ffi::OsStr::new(s)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
}
|
||||
@@ -1,21 +1,32 @@
|
||||
use crate::border::Border;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::ZOrder;
|
||||
use crate::border_manager::IMPLEMENTATION;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::colour::Colour;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::monitor_reconciliator;
|
||||
use crate::ring::Ring;
|
||||
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
|
||||
use crate::stackbar_manager::STACKBAR_LABEL;
|
||||
use crate::stackbar_manager::STACKBAR_MODE;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
|
||||
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::transparency_manager;
|
||||
use crate::window;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::ACTIVE_WINDOW_BORDER_STYLE;
|
||||
use crate::BORDER_COLOUR_CURRENT;
|
||||
use crate::BORDER_COLOUR_MONOCLE;
|
||||
use crate::BORDER_COLOUR_SINGLE;
|
||||
use crate::BORDER_COLOUR_STACK;
|
||||
use crate::BORDER_ENABLED;
|
||||
use crate::BORDER_HWND;
|
||||
use crate::BORDER_OFFSET;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_ENABLED;
|
||||
use crate::ANIMATION_FPS;
|
||||
use crate::ANIMATION_STYLE;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
@@ -27,14 +38,12 @@ use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::STACKBAR_MODE;
|
||||
use crate::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::STACKBAR_TAB_HEIGHT;
|
||||
use crate::STACKBAR_TAB_WIDTH;
|
||||
use crate::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WINDOWS_11;
|
||||
use crate::WORKSPACE_RULES;
|
||||
use komorebi_core::BorderImplementation;
|
||||
use komorebi_core::StackbarLabel;
|
||||
use komorebi_core::StackbarMode;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
@@ -47,7 +56,9 @@ use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::resolve_home_path;
|
||||
use komorebi_core::AnimationStyle;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::BorderStyle;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
@@ -73,27 +84,18 @@ use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ActiveWindowBorderColours {
|
||||
pub struct BorderColours {
|
||||
/// Border colour when the container contains a single window
|
||||
pub single: Colour,
|
||||
pub single: Option<Colour>,
|
||||
/// Border colour when the container contains multiple windows
|
||||
pub stack: Colour,
|
||||
pub stack: Option<Colour>,
|
||||
/// Border colour when the container is in monocle mode
|
||||
pub monocle: Colour,
|
||||
pub monocle: Option<Colour>,
|
||||
/// Border colour when the container is unfocused
|
||||
pub unfocused: Option<Colour>,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum ActiveWindowBorderStyle {
|
||||
#[default]
|
||||
/// Use the system border style
|
||||
System,
|
||||
/// Use the Windows 11-style rounded borders
|
||||
Rounded,
|
||||
/// Use the Windows 10-style square borders
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WorkspaceConfig {
|
||||
/// Name
|
||||
pub name: String,
|
||||
@@ -121,6 +123,9 @@ pub struct WorkspaceConfig {
|
||||
/// Permanent workspace application rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub workspace_rules: Option<Vec<IdWithIdentifier>>,
|
||||
/// Apply this monitor's window-based work area offset (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub apply_window_based_work_area_offset: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<&Workspace> for WorkspaceConfig {
|
||||
@@ -203,17 +208,24 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
workspace_padding,
|
||||
initial_workspace_rules: initial_ws_rules,
|
||||
workspace_rules: ws_rules,
|
||||
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MonitorConfig {
|
||||
/// Workspace configurations
|
||||
pub workspaces: Vec<WorkspaceConfig>,
|
||||
/// Monitor-specific work area offset (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub work_area_offset: Option<Rect>,
|
||||
/// Window based work area offset (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_based_work_area_offset: Option<Rect>,
|
||||
/// Open window limit after which the window based work area offset will no longer be applied (default: 1)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_based_work_area_offset_limit: Option<isize>,
|
||||
}
|
||||
|
||||
impl From<&Monitor> for MonitorConfig {
|
||||
@@ -226,16 +238,24 @@ impl From<&Monitor> for MonitorConfig {
|
||||
Self {
|
||||
workspaces,
|
||||
work_area_offset: value.work_area_offset(),
|
||||
window_based_work_area_offset: value.window_based_work_area_offset(),
|
||||
window_based_work_area_offset_limit: Some(value.window_based_work_area_offset_limit()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.24`
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.28`
|
||||
pub struct StaticConfig {
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub invisible_borders: Option<Rect>,
|
||||
/// DISCOURAGED: Minimum width for a window to be eligible for tiling
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum_window_width: Option<i32>,
|
||||
/// DISCOURAGED: Minimum height for a window to be eligible for tiling
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum_window_height: Option<i32>,
|
||||
/// Delta to resize windows by (default 50)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resize_delta: Option<i32>,
|
||||
@@ -267,13 +287,28 @@ pub struct StaticConfig {
|
||||
pub border_offset: Option<i32>,
|
||||
/// Display an active window border (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border: Option<bool>,
|
||||
#[serde(alias = "active_window_border")]
|
||||
pub border: Option<bool>,
|
||||
/// Active window border colours for different container types
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_colours: Option<ActiveWindowBorderColours>,
|
||||
#[serde(alias = "active_window_border_colours")]
|
||||
pub border_colours: Option<BorderColours>,
|
||||
/// Active window border style (default: System)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_style: Option<ActiveWindowBorderStyle>,
|
||||
#[serde(alias = "active_window_border_style")]
|
||||
pub border_style: Option<BorderStyle>,
|
||||
/// Active window border z-order (default: System)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_z_order: Option<ZOrder>,
|
||||
/// Display an active window border (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_implementation: Option<BorderImplementation>,
|
||||
/// Add transparency to unfocused windows (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transparency: Option<bool>,
|
||||
/// Alpha value for unfocused window transparency [[0-255]] (default: 200)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transparency_alpha: Option<u8>,
|
||||
/// Global default workspace padding (default: 10)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_workspace_padding: Option<i32>,
|
||||
@@ -283,7 +318,7 @@ pub struct StaticConfig {
|
||||
/// Monitor and workspace configurations
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitors: Option<Vec<MonitorConfig>>,
|
||||
/// Which Windows signal to use when hiding windows (default: minimize)
|
||||
/// Which Windows signal to use when hiding windows (default: Cloak)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_hiding_behaviour: Option<HidingBehaviour>,
|
||||
/// Global work area (space used for tiling) offset (default: None)
|
||||
@@ -313,30 +348,106 @@ pub struct StaticConfig {
|
||||
/// Set display index preferences
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_index_preferences: Option<HashMap<usize, String>>,
|
||||
/// Stackbar configuration options
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stackbar: Option<StackbarConfig>,
|
||||
/// Animations configuration options
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub animation: Option<AnimationsConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum StackbarMode {
|
||||
Always,
|
||||
Never,
|
||||
OnStack,
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AnimationsConfig {
|
||||
/// Enable or disable animations (default: false)
|
||||
enabled: bool,
|
||||
/// Set the animation duration in ms (default: 250)
|
||||
duration: Option<u64>,
|
||||
/// Set the animation style (default: Linear)
|
||||
style: Option<AnimationStyle>,
|
||||
/// Set the animation FPS (default: 60)
|
||||
fps: Option<u64>,
|
||||
}
|
||||
|
||||
impl StaticConfig {
|
||||
pub fn aliases(raw: &str) {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("border", ["active_window_border"]);
|
||||
map.insert("border_width", ["active_window_border_width"]);
|
||||
map.insert("border_offset", ["active_window_border_offset"]);
|
||||
map.insert("border_colours", ["active_window_border_colours"]);
|
||||
map.insert("border_style", ["active_window_border_style"]);
|
||||
|
||||
let mut display = false;
|
||||
|
||||
for aliases in map.values() {
|
||||
for a in aliases {
|
||||
if raw.contains(a) {
|
||||
display = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if display {
|
||||
println!("\nYour configuration file contains some options that have been renamed or deprecated:\n");
|
||||
for (canonical, aliases) in map {
|
||||
for alias in aliases {
|
||||
if raw.contains(alias) {
|
||||
println!(r#""{alias}" is now "{canonical}""#);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deprecated(raw: &str) {
|
||||
let deprecated_options = ["invisible_borders"];
|
||||
let deprecated_variants = vec![
|
||||
("Hide", "window_hiding_behaviour", "Cloak"),
|
||||
("Minimize", "window_hiding_behaviour", "Cloak"),
|
||||
];
|
||||
|
||||
for option in deprecated_options {
|
||||
if raw.contains(option) {
|
||||
println!(r#""{option}" is deprecated and can be removed"#);
|
||||
}
|
||||
}
|
||||
|
||||
for (variant, option, recommended) in deprecated_variants {
|
||||
if raw.contains(option) && raw.contains(variant) {
|
||||
println!(
|
||||
r#"The "{variant}" option for "{option}" is deprecated and can be removed or replaced with "{recommended}""#
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TabsConfig {
|
||||
/// Width of a stackbar tab
|
||||
width: Option<i32>,
|
||||
/// Focused tab text colour
|
||||
focused_text: Option<Colour>,
|
||||
/// Unfocused tab text colour
|
||||
unfocused_text: Option<Colour>,
|
||||
/// Tab background colour
|
||||
background: Option<Colour>,
|
||||
/// Font family
|
||||
font_family: Option<String>,
|
||||
/// Font size
|
||||
font_size: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StackbarConfig {
|
||||
height: Option<i32>,
|
||||
mode: Option<StackbarMode>,
|
||||
tabs: Option<TabsConfig>,
|
||||
/// Stackbar height
|
||||
pub height: Option<i32>,
|
||||
/// Stackbar height
|
||||
pub label: Option<StackbarLabel>,
|
||||
/// Stackbar mode
|
||||
pub mode: Option<StackbarMode>,
|
||||
/// Stackbar tab configuration options
|
||||
pub tabs: Option<TabsConfig>,
|
||||
}
|
||||
|
||||
impl From<&WindowManager> for StaticConfig {
|
||||
@@ -348,6 +459,8 @@ impl From<&WindowManager> for StaticConfig {
|
||||
}
|
||||
|
||||
let mut to_remove = vec![];
|
||||
let mut to_add_initial = vec![];
|
||||
let mut to_add_persistent = vec![];
|
||||
|
||||
let workspace_rules = WORKSPACE_RULES.lock();
|
||||
for (m_idx, m) in monitors.iter().enumerate() {
|
||||
@@ -364,6 +477,12 @@ impl From<&WindowManager> for StaticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
for (identifier, (monitor_idx, workspace_idx, initial)) in &*workspace_rules {
|
||||
if *initial && (*monitor_idx == m_idx && *workspace_idx == w_idx) {
|
||||
to_add_initial.push((m_idx, w_idx, identifier.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rules) = &w.workspace_rules {
|
||||
for wsr in rules {
|
||||
for (identifier, (monitor_idx, workspace_idx, _)) in &*workspace_rules {
|
||||
@@ -375,38 +494,69 @@ impl From<&WindowManager> for StaticConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (identifier, (monitor_idx, workspace_idx, initial)) in &*workspace_rules {
|
||||
if !*initial && (*monitor_idx == m_idx && *workspace_idx == w_idx) {
|
||||
to_add_persistent.push((m_idx, w_idx, identifier.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (m_idx, w_idx, id) in to_remove {
|
||||
if let Some(monitor) = monitors.get_mut(m_idx) {
|
||||
if let Some(workspace) = monitor.workspaces.get_mut(w_idx) {
|
||||
if workspace.workspace_rules.is_none() {
|
||||
workspace.workspace_rules = Some(vec![]);
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut workspace.workspace_rules {
|
||||
rules.retain(|r| r.id != id);
|
||||
for (monitor_idx, workspace_idx, id) in &to_add_persistent {
|
||||
if m_idx == *monitor_idx && w_idx == *workspace_idx {
|
||||
rules.push(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: id.clone(),
|
||||
matching_strategy: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
rules.dedup();
|
||||
}
|
||||
|
||||
if workspace.initial_workspace_rules.is_none() {
|
||||
workspace.workspace_rules = Some(vec![]);
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut workspace.initial_workspace_rules {
|
||||
rules.retain(|r| r.id != id);
|
||||
for (monitor_idx, workspace_idx, id) in &to_add_initial {
|
||||
if m_idx == *monitor_idx && w_idx == *workspace_idx {
|
||||
rules.push(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: id.clone(),
|
||||
matching_strategy: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
rules.dedup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let border_colours = if BORDER_COLOUR_SINGLE.load(Ordering::SeqCst) == 0 {
|
||||
let border_colours = if border_manager::FOCUSED.load(Ordering::SeqCst) == 0 {
|
||||
None
|
||||
} else {
|
||||
Option::from(ActiveWindowBorderColours {
|
||||
single: Colour::from(BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)),
|
||||
stack: Colour::from(if BORDER_COLOUR_STACK.load(Ordering::SeqCst) == 0 {
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
|
||||
} else {
|
||||
BORDER_COLOUR_STACK.load(Ordering::SeqCst)
|
||||
}),
|
||||
monocle: Colour::from(if BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst) == 0 {
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
|
||||
} else {
|
||||
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst)
|
||||
}),
|
||||
Option::from(BorderColours {
|
||||
single: Option::from(Colour::from(border_manager::FOCUSED.load(Ordering::SeqCst))),
|
||||
stack: Option::from(Colour::from(border_manager::STACK.load(Ordering::SeqCst))),
|
||||
monocle: Option::from(Colour::from(border_manager::MONOCLE.load(Ordering::SeqCst))),
|
||||
unfocused: Option::from(Colour::from(
|
||||
border_manager::UNFOCUSED.load(Ordering::SeqCst),
|
||||
)),
|
||||
})
|
||||
};
|
||||
|
||||
@@ -418,14 +568,24 @@ impl From<&WindowManager> for StaticConfig {
|
||||
unmanaged_window_operation_behaviour: Option::from(
|
||||
value.unmanaged_window_operation_behaviour,
|
||||
),
|
||||
minimum_window_height: Some(window::MINIMUM_HEIGHT.load(Ordering::SeqCst)),
|
||||
minimum_window_width: Some(window::MINIMUM_WIDTH.load(Ordering::SeqCst)),
|
||||
focus_follows_mouse: value.focus_follows_mouse,
|
||||
mouse_follows_focus: Option::from(value.mouse_follows_focus),
|
||||
app_specific_configuration_path: None,
|
||||
border_width: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
|
||||
border_offset: Option::from(BORDER_OFFSET.load(Ordering::SeqCst)),
|
||||
active_window_border: Option::from(BORDER_ENABLED.load(Ordering::SeqCst)),
|
||||
active_window_border_colours: border_colours,
|
||||
active_window_border_style: Option::from(*ACTIVE_WINDOW_BORDER_STYLE.lock()),
|
||||
border_width: Option::from(border_manager::BORDER_WIDTH.load(Ordering::SeqCst)),
|
||||
border_offset: Option::from(border_manager::BORDER_OFFSET.load(Ordering::SeqCst)),
|
||||
border: Option::from(border_manager::BORDER_ENABLED.load(Ordering::SeqCst)),
|
||||
border_colours,
|
||||
transparency: Option::from(
|
||||
transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst),
|
||||
),
|
||||
transparency_alpha: Option::from(
|
||||
transparency_manager::TRANSPARENCY_ALPHA.load(Ordering::SeqCst),
|
||||
),
|
||||
border_style: Option::from(STYLE.load()),
|
||||
border_z_order: Option::from(Z_ORDER.load()),
|
||||
border_implementation: Option::from(IMPLEMENTATION.load()),
|
||||
default_workspace_padding: Option::from(
|
||||
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
|
||||
),
|
||||
@@ -444,6 +604,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
|
||||
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()),
|
||||
stackbar: None,
|
||||
animation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,12 +614,12 @@ impl StaticConfig {
|
||||
fn apply_globals(&mut self) -> Result<()> {
|
||||
if let Some(monitor_index_preferences) = &self.monitor_index_preferences {
|
||||
let mut preferences = MONITOR_INDEX_PREFERENCES.lock();
|
||||
*preferences = monitor_index_preferences.clone();
|
||||
preferences.clone_from(monitor_index_preferences);
|
||||
}
|
||||
|
||||
if let Some(display_index_preferences) = &self.display_index_preferences {
|
||||
let mut preferences = DISPLAY_INDEX_PREFERENCES.lock();
|
||||
*preferences = display_index_preferences.clone();
|
||||
preferences.clone_from(display_index_preferences);
|
||||
}
|
||||
|
||||
if let Some(behaviour) = self.window_hiding_behaviour {
|
||||
@@ -466,6 +627,22 @@ impl StaticConfig {
|
||||
*window_hiding_behaviour = behaviour;
|
||||
}
|
||||
|
||||
if let Some(height) = self.minimum_window_height {
|
||||
window::MINIMUM_HEIGHT.store(height, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(width) = self.minimum_window_width {
|
||||
window::MINIMUM_WIDTH.store(width, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(animations) = &self.animation {
|
||||
ANIMATION_ENABLED.store(animations.enabled, Ordering::SeqCst);
|
||||
ANIMATION_DURATION.store(animations.duration.unwrap_or(250), Ordering::SeqCst);
|
||||
ANIMATION_FPS.store(animations.fps.unwrap_or(60), Ordering::SeqCst);
|
||||
let mut animation_style = ANIMATION_STYLE.lock();
|
||||
*animation_style = animations.style.unwrap_or(AnimationStyle::Linear);
|
||||
}
|
||||
|
||||
if let Some(container) = self.default_container_padding {
|
||||
DEFAULT_CONTAINER_PADDING.store(container, Ordering::SeqCst);
|
||||
}
|
||||
@@ -474,26 +651,60 @@ impl StaticConfig {
|
||||
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
self.border_width.map_or_else(
|
||||
|| {
|
||||
BORDER_WIDTH.store(8, Ordering::SeqCst);
|
||||
},
|
||||
|width| {
|
||||
BORDER_WIDTH.store(width, Ordering::SeqCst);
|
||||
},
|
||||
);
|
||||
border_manager::BORDER_WIDTH.store(self.border_width.unwrap_or(8), Ordering::SeqCst);
|
||||
border_manager::BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
|
||||
|
||||
BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
|
||||
|
||||
if let Some(colours) = &self.active_window_border_colours {
|
||||
BORDER_COLOUR_SINGLE.store(u32::from(colours.single), Ordering::SeqCst);
|
||||
BORDER_COLOUR_CURRENT.store(u32::from(colours.single), Ordering::SeqCst);
|
||||
BORDER_COLOUR_STACK.store(u32::from(colours.stack), Ordering::SeqCst);
|
||||
BORDER_COLOUR_MONOCLE.store(u32::from(colours.monocle), Ordering::SeqCst);
|
||||
if let Some(enabled) = &self.border {
|
||||
border_manager::BORDER_ENABLED.store(*enabled, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let active_window_border_style = self.active_window_border_style.unwrap_or_default();
|
||||
*ACTIVE_WINDOW_BORDER_STYLE.lock() = active_window_border_style;
|
||||
if let Some(colours) = &self.border_colours {
|
||||
if let Some(single) = colours.single {
|
||||
border_manager::FOCUSED.store(u32::from(single), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(stack) = colours.stack {
|
||||
border_manager::STACK.store(u32::from(stack), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(monocle) = colours.monocle {
|
||||
border_manager::MONOCLE.store(u32::from(monocle), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(unfocused) = colours.unfocused {
|
||||
border_manager::UNFOCUSED.store(u32::from(unfocused), Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
STYLE.store(self.border_style.unwrap_or_default());
|
||||
|
||||
if !*WINDOWS_11
|
||||
&& matches!(
|
||||
self.border_implementation.unwrap_or_default(),
|
||||
BorderImplementation::Windows
|
||||
)
|
||||
{
|
||||
tracing::error!(
|
||||
"BorderImplementation::Windows is only supported on Windows 11 and above"
|
||||
);
|
||||
} else {
|
||||
IMPLEMENTATION.store(self.border_implementation.unwrap_or_default());
|
||||
match IMPLEMENTATION.load() {
|
||||
BorderImplementation::Komorebi => {
|
||||
border_manager::destroy_all_borders()?;
|
||||
}
|
||||
BorderImplementation::Windows => {
|
||||
// TODO: figure out how to call wm.remove_all_accents here
|
||||
}
|
||||
}
|
||||
|
||||
border_manager::send_notification();
|
||||
}
|
||||
|
||||
transparency_manager::TRANSPARENCY_ENABLED
|
||||
.store(self.transparency.unwrap_or(false), Ordering::SeqCst);
|
||||
transparency_manager::TRANSPARENCY_ALPHA
|
||||
.store(self.transparency_alpha.unwrap_or(200), Ordering::SeqCst);
|
||||
|
||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
@@ -534,10 +745,16 @@ impl StaticConfig {
|
||||
if let Some(height) = &stackbar.height {
|
||||
STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst);
|
||||
}
|
||||
if let Some(mode) = &stackbar.mode {
|
||||
let mut stackbar_mode = STACKBAR_MODE.lock();
|
||||
*stackbar_mode = *mode;
|
||||
|
||||
if let Some(label) = &stackbar.label {
|
||||
STACKBAR_LABEL.store(*label);
|
||||
}
|
||||
|
||||
if let Some(mode) = &stackbar.mode {
|
||||
STACKBAR_MODE.store(*mode);
|
||||
}
|
||||
|
||||
#[allow(clippy::assigning_clones)]
|
||||
if let Some(tabs) = &stackbar.tabs {
|
||||
if let Some(background) = &tabs.background {
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.store((*background).into(), Ordering::SeqCst);
|
||||
@@ -554,6 +771,9 @@ impl StaticConfig {
|
||||
if let Some(width) = &tabs.width {
|
||||
STACKBAR_TAB_WIDTH.store(*width, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
STACKBAR_FONT_SIZE.store(tabs.font_size.unwrap_or(0), Ordering::SeqCst);
|
||||
*STACKBAR_FONT_FAMILY.lock() = tabs.font_family.clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,7 +855,6 @@ impl StaticConfig {
|
||||
|
||||
let mut wm = WindowManager {
|
||||
monitors: Ring::default(),
|
||||
monitor_cache: HashMap::new(),
|
||||
incoming_events: incoming,
|
||||
command_listener: listener,
|
||||
is_paused: false,
|
||||
@@ -693,9 +912,20 @@ impl StaticConfig {
|
||||
|
||||
if let Some(monitors) = value.monitors {
|
||||
for (i, monitor) in monitors.iter().enumerate() {
|
||||
{
|
||||
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
|
||||
if let Some(device_id) = display_index_preferences.get(&i) {
|
||||
monitor_reconciliator::insert_in_monitor_cache(device_id, monitor.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(m) = wm.monitors_mut().get_mut(i) {
|
||||
m.ensure_workspace_count(monitor.workspaces.len());
|
||||
m.set_work_area_offset(monitor.work_area_offset);
|
||||
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
|
||||
m.set_window_based_work_area_offset_limit(
|
||||
monitor.window_based_work_area_offset_limit.unwrap_or(1),
|
||||
);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
ws.load_static_config(
|
||||
@@ -723,13 +953,8 @@ impl StaticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
if value.active_window_border == Some(true) {
|
||||
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
|
||||
Border::create("komorebi-border-window")?;
|
||||
}
|
||||
|
||||
BORDER_ENABLED.store(true, Ordering::SeqCst);
|
||||
wm.show_border()?;
|
||||
if value.border == Some(true) {
|
||||
border_manager::BORDER_ENABLED.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -746,6 +971,10 @@ impl StaticConfig {
|
||||
if let Some(m) = wm.monitors_mut().get_mut(i) {
|
||||
m.ensure_workspace_count(monitor.workspaces.len());
|
||||
m.set_work_area_offset(monitor.work_area_offset);
|
||||
m.set_window_based_work_area_offset(monitor.window_based_work_area_offset);
|
||||
m.set_window_based_work_area_offset_limit(
|
||||
monitor.window_based_work_area_offset_limit.unwrap_or(1),
|
||||
);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
ws.load_static_config(
|
||||
@@ -773,16 +1002,8 @@ impl StaticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
if value.active_window_border == Some(true) {
|
||||
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
|
||||
Border::create("komorebi-border-window")?;
|
||||
}
|
||||
|
||||
BORDER_ENABLED.store(true, Ordering::SeqCst);
|
||||
wm.show_border()?;
|
||||
} else {
|
||||
BORDER_ENABLED.store(false, Ordering::SeqCst);
|
||||
wm.hide_border()?;
|
||||
if let Some(enabled) = value.border {
|
||||
border_manager::BORDER_ENABLED.store(enabled, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(val) = value.window_container_behaviour {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use bitflags::bitflags;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_BORDER;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_CAPTION;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
|
||||
@@ -56,7 +58,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_VSCROLL;
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct WindowStyle: u32 {
|
||||
const BORDER = WS_BORDER.0;
|
||||
const CAPTION = WS_CAPTION.0;
|
||||
@@ -90,7 +92,7 @@ bitflags! {
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct ExtendedWindowStyle: u32 {
|
||||
const ACCEPTFILES = WS_EX_ACCEPTFILES.0;
|
||||
const APPWINDOW = WS_EX_APPWINDOW.0;
|
||||
|
||||
179
komorebi/src/transparency_manager.rs
Normal file
179
komorebi/src/transparency_manager.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicU8;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
use crate::Window;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
|
||||
pub static TRANSPARENCY_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
pub static TRANSPARENCY_ALPHA: AtomicU8 = AtomicU8::new(200);
|
||||
|
||||
static KNOWN_HWNDS: OnceLock<Mutex<Vec<isize>>> = OnceLock::new();
|
||||
|
||||
pub struct Notification;
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn known_hwnds() -> Vec<isize> {
|
||||
let known = KNOWN_HWNDS.get_or_init(|| Mutex::new(Vec::new())).lock();
|
||||
known.iter().copied().collect()
|
||||
}
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification() {
|
||||
if event_tx().try_send(Notification).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
event_tx().send(Notification)?;
|
||||
|
||||
'receiver: for _ in receiver {
|
||||
let known_hwnds = KNOWN_HWNDS.get_or_init(|| Mutex::new(Vec::new()));
|
||||
if !TRANSPARENCY_ENABLED.load_consume() {
|
||||
for hwnd in known_hwnds.lock().iter() {
|
||||
if let Err(error) = Window::from(*hwnd).opaque() {
|
||||
tracing::error!("failed to make window {hwnd} opaque: {error}")
|
||||
}
|
||||
}
|
||||
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
known_hwnds.lock().clear();
|
||||
|
||||
// Check the wm state every time we receive a notification
|
||||
let state = wm.lock();
|
||||
|
||||
let focused_monitor_idx = state.focused_monitor_idx();
|
||||
|
||||
'monitors: for (monitor_idx, m) in state.monitors.elements().iter().enumerate() {
|
||||
let focused_workspace_idx = m.focused_workspace_idx();
|
||||
|
||||
'workspaces: for (workspace_idx, ws) in m.workspaces().iter().enumerate() {
|
||||
// Only operate on the focused workspace of each monitor
|
||||
// Workspaces with tiling disabled don't have transparent windows
|
||||
if !ws.tile() || workspace_idx != focused_workspace_idx {
|
||||
for window in ws.visible_windows().iter().flatten() {
|
||||
if let Err(error) = window.opaque() {
|
||||
let hwnd = window.hwnd;
|
||||
tracing::error!("failed to make window {hwnd} opaque: {error}")
|
||||
}
|
||||
}
|
||||
|
||||
continue 'workspaces;
|
||||
}
|
||||
|
||||
// Monocle container is never transparent
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
if let Err(error) = window.opaque() {
|
||||
let hwnd = window.hwnd;
|
||||
tracing::error!("failed to make monocle window {hwnd} opaque: {error}")
|
||||
}
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let is_maximized = WindowsApi::is_zoomed(HWND(foreground_hwnd));
|
||||
|
||||
if is_maximized {
|
||||
if let Err(error) = Window::from(foreground_hwnd).opaque() {
|
||||
let hwnd = foreground_hwnd;
|
||||
tracing::error!("failed to make maximized window {hwnd} opaque: {error}")
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
// Update the transparency for all containers on this workspace
|
||||
|
||||
// If the window is not focused on the current workspace, or isn't on the focused monitor
|
||||
// make it transparent
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if idx != ws.focused_container_idx() || monitor_idx != focused_monitor_idx {
|
||||
let focused_window_idx = c.focused_window_idx();
|
||||
for (window_idx, window) in c.windows().iter().enumerate() {
|
||||
if window_idx == focused_window_idx {
|
||||
match window.transparent() {
|
||||
Err(error) => {
|
||||
let hwnd = foreground_hwnd;
|
||||
tracing::error!(
|
||||
"failed to make unfocused window {hwnd} transparent: {error}"
|
||||
)
|
||||
}
|
||||
Ok(..) => {
|
||||
known_hwnds.lock().push(window.hwnd);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// just in case, this is useful when people are clicking around
|
||||
// on unfocused stackbar tabs
|
||||
known_hwnds.lock().push(window.hwnd);
|
||||
}
|
||||
}
|
||||
// Otherwise, make it opaque
|
||||
} else {
|
||||
let focused_window_idx = c.focused_window_idx();
|
||||
for (window_idx, window) in c.windows().iter().enumerate() {
|
||||
if window_idx != focused_window_idx {
|
||||
known_hwnds.lock().push(window.hwnd);
|
||||
} else {
|
||||
if let Err(error) =
|
||||
c.focused_window().copied().unwrap_or_default().opaque()
|
||||
{
|
||||
let hwnd = foreground_hwnd;
|
||||
tracing::error!(
|
||||
"failed to make focused window {hwnd} opaque: {error}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,20 +1,28 @@
|
||||
use crate::border_manager;
|
||||
use crate::com::SetCloak;
|
||||
use crate::focus_manager;
|
||||
use crate::stackbar_manager;
|
||||
use crate::ANIMATIONS_IN_PROGRESS;
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_ENABLED;
|
||||
use crate::ANIMATION_TEMPORARY_DISABLED;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::ser::Error;
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -25,8 +33,10 @@ use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::styles::ExtendedWindowStyle;
|
||||
use crate::styles::WindowStyle;
|
||||
use crate::transparency_manager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
@@ -39,9 +49,31 @@ use crate::PERMAIGNORE_CLASSES;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::WSL2_UI_PROCESSES;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
|
||||
pub static MINIMUM_WIDTH: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Window {
|
||||
pub(crate) hwnd: isize,
|
||||
pub hwnd: isize,
|
||||
animation: Animation,
|
||||
}
|
||||
|
||||
impl From<isize> for Window {
|
||||
fn from(value: isize) -> Self {
|
||||
Self {
|
||||
hwnd: value,
|
||||
animation: Animation::new(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HWND> for Window {
|
||||
fn from(value: HWND) -> Self {
|
||||
Self {
|
||||
hwnd: value.0,
|
||||
animation: Animation::new(value.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
@@ -97,24 +129,23 @@ impl Serialize for Window {
|
||||
"title",
|
||||
&self
|
||||
.title()
|
||||
.map_err(|_| S::Error::custom("could not get window title"))?,
|
||||
.unwrap_or_else(|_| String::from("could not get window title")),
|
||||
)?;
|
||||
state.serialize_field(
|
||||
"exe",
|
||||
&self
|
||||
.exe()
|
||||
.map_err(|_| S::Error::custom("could not get window exe"))?,
|
||||
.unwrap_or_else(|_| String::from("could not get window exe")),
|
||||
)?;
|
||||
state.serialize_field(
|
||||
"class",
|
||||
&self
|
||||
.class()
|
||||
.map_err(|_| S::Error::custom("could not get window class"))?,
|
||||
.unwrap_or_else(|_| String::from("could not get window class")),
|
||||
)?;
|
||||
state.serialize_field(
|
||||
"rect",
|
||||
&WindowsApi::window_rect(self.hwnd())
|
||||
.map_err(|_| S::Error::custom("could not get window rect"))?,
|
||||
&WindowsApi::window_rect(self.hwnd()).unwrap_or_default(),
|
||||
)?;
|
||||
state.end()
|
||||
}
|
||||
@@ -140,9 +171,66 @@ impl Window {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_position(&mut self, layout: &Rect, top: bool) -> Result<()> {
|
||||
let rect = *layout;
|
||||
WindowsApi::position_window(self.hwnd(), &rect, top)
|
||||
pub fn animate_position(&self, layout: &Rect, top: bool) -> Result<()> {
|
||||
let hwnd = self.hwnd();
|
||||
let curr_rect = WindowsApi::window_rect(hwnd).unwrap();
|
||||
|
||||
let target_rect = *layout;
|
||||
let duration = Duration::from_millis(ANIMATION_DURATION.load(Ordering::SeqCst));
|
||||
let mut animation = self.animation;
|
||||
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
border_manager::send_notification();
|
||||
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
animation.animate(duration, |progress: f64| {
|
||||
let new_rect = Animation::lerp_rect(&curr_rect, &target_rect, progress);
|
||||
|
||||
if progress == 1.0 {
|
||||
WindowsApi::position_window(hwnd, &new_rect, top)?;
|
||||
if WindowsApi::foreground_window().unwrap_or_default() == hwnd.0 {
|
||||
focus_manager::send_notification(hwnd.0)
|
||||
}
|
||||
|
||||
if ANIMATIONS_IN_PROGRESS.load(Ordering::Acquire) == 0 {
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED
|
||||
.store(false, Ordering::SeqCst);
|
||||
|
||||
border_manager::send_notification();
|
||||
stackbar_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
}
|
||||
} else {
|
||||
// using MoveWindow because it runs faster than SetWindowPos
|
||||
// so animation have more fps and feel smoother
|
||||
WindowsApi::move_window(hwnd, &new_rect, false)?;
|
||||
// WindowsApi::position_window(hwnd, &new_rect, top)?;
|
||||
WindowsApi::invalidate_rect(hwnd, None, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
|
||||
if WindowsApi::window_rect(self.hwnd())?.eq(layout) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if ANIMATION_ENABLED.load(Ordering::SeqCst)
|
||||
&& !ANIMATION_TEMPORARY_DISABLED.load(Ordering::SeqCst)
|
||||
{
|
||||
self.animate_position(layout, top)
|
||||
} else {
|
||||
WindowsApi::position_window(self.hwnd(), layout, top)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_maximized(self) -> bool {
|
||||
@@ -153,6 +241,10 @@ impl Window {
|
||||
WindowsApi::is_iconic(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn is_visible(self) -> bool {
|
||||
WindowsApi::is_window_visible(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn hide(self) {
|
||||
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
if !programmatically_hidden_hwnds.contains(&self.hwnd) {
|
||||
@@ -218,77 +310,25 @@ impl Window {
|
||||
}
|
||||
|
||||
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();
|
||||
// If the target window is already focused, do nothing.
|
||||
if let Ok(ihwnd) = WindowsApi::foreground_window() {
|
||||
if HWND(ihwnd) == self.hwnd() {
|
||||
// Center cursor in Window
|
||||
if mouse_follows_focus {
|
||||
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
|
||||
}
|
||||
|
||||
// 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
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Raise Window to foreground
|
||||
let mut foregrounded = false;
|
||||
let mut tried_resetting_foreground_access = false;
|
||||
let mut max_attempts = 10;
|
||||
|
||||
while !foregrounded && max_attempts > 0 {
|
||||
match WindowsApi::set_foreground_window(self.hwnd()) {
|
||||
Ok(()) => {
|
||||
foregrounded = true;
|
||||
}
|
||||
Err(error) => {
|
||||
max_attempts -= 1;
|
||||
tracing::error!(
|
||||
"could not set as foreground window, but continuing execution of focus(): {}",
|
||||
error
|
||||
);
|
||||
|
||||
// If this still doesn't work then maybe try https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-locksetforegroundwindow
|
||||
if !tried_resetting_foreground_access {
|
||||
let process_id = WindowsApi::current_process_id();
|
||||
if WindowsApi::allow_set_foreground_window(process_id).is_ok() {
|
||||
tried_resetting_foreground_access = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
WindowsApi::raise_and_focus_window(self.hwnd())?;
|
||||
|
||||
// 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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, false) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not detach from window thread input processing mechanism, but continuing execution of focus(): {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -296,7 +336,10 @@ impl Window {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.insert(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)?;
|
||||
WindowsApi::set_transparent(self.hwnd())
|
||||
WindowsApi::set_transparent(
|
||||
self.hwnd(),
|
||||
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn opaque(self) -> Result<()> {
|
||||
@@ -305,6 +348,14 @@ impl Window {
|
||||
self.update_ex_style(&ex_style)
|
||||
}
|
||||
|
||||
pub fn set_accent(self, colour: u32) -> Result<()> {
|
||||
WindowsApi::set_window_accent(self.hwnd, Some(colour))
|
||||
}
|
||||
|
||||
pub fn remove_accent(self) -> Result<()> {
|
||||
WindowsApi::set_window_accent(self.hwnd, None)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn update_style(self, style: &WindowStyle) -> Result<()> {
|
||||
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
|
||||
@@ -316,12 +367,12 @@ impl Window {
|
||||
|
||||
pub fn style(self) -> Result<WindowStyle> {
|
||||
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?;
|
||||
WindowStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
|
||||
Ok(WindowStyle::from_bits_truncate(bits))
|
||||
}
|
||||
|
||||
pub fn ex_style(self) -> Result<ExtendedWindowStyle> {
|
||||
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?;
|
||||
ExtendedWindowStyle::from_bits(bits).ok_or_else(|| anyhow!("there is no gwl style"))
|
||||
Ok(ExtendedWindowStyle::from_bits_truncate(bits))
|
||||
}
|
||||
|
||||
pub fn title(self) -> Result<String> {
|
||||
@@ -370,18 +421,41 @@ impl Window {
|
||||
self.update_style(&style)
|
||||
}
|
||||
|
||||
#[tracing::instrument(fields(exe, title))]
|
||||
pub fn should_manage(self, event: Option<WindowManagerEvent>) -> Result<bool> {
|
||||
if let Some(WindowManagerEvent::DisplayChange(_)) = event {
|
||||
return Ok(true);
|
||||
#[tracing::instrument(fields(exe, title), skip(debug))]
|
||||
pub fn should_manage(
|
||||
self,
|
||||
event: Option<WindowManagerEvent>,
|
||||
debug: &mut RuleDebug,
|
||||
) -> Result<bool> {
|
||||
if !self.is_window() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
#[allow(clippy::question_mark)]
|
||||
debug.is_window = true;
|
||||
|
||||
let rect = WindowsApi::window_rect(self.hwnd()).unwrap_or_default();
|
||||
|
||||
if rect.right < MINIMUM_WIDTH.load(Ordering::SeqCst) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
debug.has_minimum_width = true;
|
||||
|
||||
if rect.bottom < MINIMUM_HEIGHT.load(Ordering::SeqCst) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
debug.has_minimum_height = true;
|
||||
|
||||
if self.title().is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let is_cloaked = self.is_cloaked()?;
|
||||
debug.has_title = true;
|
||||
|
||||
let is_cloaked = self.is_cloaked().unwrap_or_default();
|
||||
|
||||
debug.is_cloaked = is_cloaked;
|
||||
|
||||
let mut allow_cloaked = false;
|
||||
|
||||
@@ -394,13 +468,28 @@ impl Window {
|
||||
}
|
||||
}
|
||||
|
||||
debug.allow_cloaked = allow_cloaked;
|
||||
|
||||
match (allow_cloaked, is_cloaked) {
|
||||
// If allowing cloaked windows, we don't need to check the cloaked status
|
||||
(true, _) |
|
||||
// If not allowing cloaked windows, we need to ensure the window is not cloaked
|
||||
(false, false) => {
|
||||
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) = (self.title(), self.exe(), self.class(), self.path()) {
|
||||
return Ok(window_is_eligible(&title, &exe_name, &class, &path, &self.style()?, &self.ex_style()?, event));
|
||||
debug.title = Some(title.clone());
|
||||
debug.exe_name = Some(exe_name.clone());
|
||||
debug.class = Some(class.clone());
|
||||
debug.path = Some(path.clone());
|
||||
// calls for styles can fail quite often for events with windows that aren't really "windows"
|
||||
// since we have moved up calls of should_manage to the beginning of the process_event handler,
|
||||
// we should handle failures here gracefully to be able to continue the execution of process_event
|
||||
if let (Ok(style), Ok(ex_style)) = (&self.style(), &self.ex_style()) {
|
||||
debug.window_style = Some(*style);
|
||||
debug.extended_window_style = Some(*ex_style);
|
||||
let eligible = window_is_eligible(self.hwnd, &title, &exe_name, &class, &path, style, ex_style, event, debug);
|
||||
debug.should_manage = eligible;
|
||||
return Ok(eligible);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -410,7 +499,33 @@ impl Window {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct RuleDebug {
|
||||
pub should_manage: bool,
|
||||
pub is_window: bool,
|
||||
pub has_minimum_width: bool,
|
||||
pub has_minimum_height: bool,
|
||||
pub has_title: bool,
|
||||
pub is_cloaked: bool,
|
||||
pub allow_cloaked: bool,
|
||||
pub allow_layered_transparency: bool,
|
||||
pub window_style: Option<WindowStyle>,
|
||||
pub extended_window_style: Option<ExtendedWindowStyle>,
|
||||
pub title: Option<String>,
|
||||
pub exe_name: Option<String>,
|
||||
pub class: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub matches_permaignore_class: Option<String>,
|
||||
pub matches_float_identifier: Option<MatchingRule>,
|
||||
pub matches_managed_override: Option<MatchingRule>,
|
||||
pub matches_layered_whitelist: Option<MatchingRule>,
|
||||
pub matches_wsl2_gui: Option<String>,
|
||||
pub matches_no_titlebar: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn window_is_eligible(
|
||||
hwnd: isize,
|
||||
title: &String,
|
||||
exe_name: &String,
|
||||
class: &String,
|
||||
@@ -418,10 +533,12 @@ fn window_is_eligible(
|
||||
style: &WindowStyle,
|
||||
ex_style: &ExtendedWindowStyle,
|
||||
event: Option<WindowManagerEvent>,
|
||||
debug: &mut RuleDebug,
|
||||
) -> bool {
|
||||
{
|
||||
let permaignore_classes = PERMAIGNORE_CLASSES.lock();
|
||||
if permaignore_classes.contains(class) {
|
||||
debug.matches_permaignore_class = Some(class.clone());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -429,45 +546,71 @@ fn window_is_eligible(
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
let float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
let should_float = should_act(
|
||||
let should_float = if let Some(rule) = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&float_identifiers,
|
||||
®ex_identifiers,
|
||||
);
|
||||
) {
|
||||
debug.matches_float_identifier = Some(rule);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
let managed_override = should_act(
|
||||
let managed_override = if let Some(rule) = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&manage_identifiers,
|
||||
®ex_identifiers,
|
||||
);
|
||||
) {
|
||||
debug.matches_managed_override = Some(rule);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if should_float && !managed_override {
|
||||
return false;
|
||||
}
|
||||
|
||||
let layered_whitelist = LAYERED_WHITELIST.lock();
|
||||
let allow_layered = should_act(
|
||||
let mut allow_layered = if let Some(rule) = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&layered_whitelist,
|
||||
®ex_identifiers,
|
||||
);
|
||||
) {
|
||||
debug.matches_layered_whitelist = Some(rule);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// TODO: might need this for transparency
|
||||
// let allow_layered = true;
|
||||
let known_layered_hwnds = transparency_manager::known_hwnds();
|
||||
|
||||
allow_layered = if known_layered_hwnds.contains(&hwnd) {
|
||||
debug.allow_layered_transparency = true;
|
||||
true
|
||||
} else {
|
||||
allow_layered
|
||||
};
|
||||
|
||||
let allow_wsl2_gui = {
|
||||
let wsl2_ui_processes = WSL2_UI_PROCESSES.lock();
|
||||
wsl2_ui_processes.contains(exe_name)
|
||||
let allow = wsl2_ui_processes.contains(exe_name);
|
||||
if allow {
|
||||
debug.matches_wsl2_gui = Some(exe_name.clone())
|
||||
}
|
||||
|
||||
allow
|
||||
};
|
||||
|
||||
let allow_titlebar_removed = {
|
||||
@@ -509,10 +652,10 @@ pub fn should_act(
|
||||
path: &str,
|
||||
identifiers: &[MatchingRule],
|
||||
regex_identifiers: &HashMap<String, Regex>,
|
||||
) -> bool {
|
||||
let mut should_act = false;
|
||||
for identifier in identifiers {
|
||||
match identifier {
|
||||
) -> Option<MatchingRule> {
|
||||
let mut matching_rule = None;
|
||||
for rule in identifiers {
|
||||
match rule {
|
||||
MatchingRule::Simple(identifier) => {
|
||||
if should_act_individual(
|
||||
title,
|
||||
@@ -522,7 +665,7 @@ pub fn should_act(
|
||||
identifier,
|
||||
regex_identifiers,
|
||||
) {
|
||||
should_act = true
|
||||
matching_rule = Some(rule.clone());
|
||||
};
|
||||
}
|
||||
MatchingRule::Composite(identifiers) => {
|
||||
@@ -539,13 +682,13 @@ pub fn should_act(
|
||||
}
|
||||
|
||||
if composite_results.iter().all(|&x| x) {
|
||||
should_act = true;
|
||||
matching_rule = Some(rule.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
should_act
|
||||
matching_rule
|
||||
}
|
||||
|
||||
pub fn should_act_individual(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user