mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-13 22:13:02 +01:00
Compare commits
169 Commits
v0.1.21
...
feature/cm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8176849dc3 | ||
|
|
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 | ||
|
|
862219b9a3 | ||
|
|
0fc75afb00 | ||
|
|
d67f355a17 | ||
|
|
3d0ed4cfc4 | ||
|
|
21be01b9aa | ||
|
|
648ba672e3 | ||
|
|
af6529851e | ||
|
|
bea3d1fcbc | ||
|
|
e40fa11b0d | ||
|
|
251ec3d53e | ||
|
|
5e714cafab | ||
|
|
6aa9be1ea0 | ||
|
|
6238d1f848 | ||
|
|
d83dc48230 | ||
|
|
f2c4dadcde | ||
|
|
c05e9ea089 | ||
|
|
37aa99a537 | ||
|
|
dca32bead5 | ||
|
|
77eaddca1e | ||
|
|
79fb098d04 | ||
|
|
d0bab4280a | ||
|
|
e616004da7 | ||
|
|
c65060fbd9 | ||
|
|
50a851a660 | ||
|
|
8ad6f4be1f | ||
|
|
24ac0c4cd3 | ||
|
|
e0652d953a | ||
|
|
d6e0ecc507 | ||
|
|
ca22cdb07f | ||
|
|
dc38eae2af | ||
|
|
66446f571c | ||
|
|
b2f6329963 | ||
|
|
bc46f65f64 | ||
|
|
69573c383f | ||
|
|
45a3f2a6b5 | ||
|
|
5af00b64cf | ||
|
|
81dff3279c | ||
|
|
2f17e4bb29 | ||
|
|
1b966d3731 | ||
|
|
e58d776f81 | ||
|
|
40e77fddfe | ||
|
|
9c8a50fe80 | ||
|
|
41e9068fca | ||
|
|
3690f8ebd8 | ||
|
|
7b24474ef2 | ||
|
|
f675717844 | ||
|
|
38b0418c3b | ||
|
|
6781f34930 | ||
|
|
4919872e1a | ||
|
|
d730c3c72d | ||
|
|
4e98d7d36d | ||
|
|
2bceff4edc | ||
|
|
b32bce8713 | ||
|
|
47af40cf9e | ||
|
|
2b9fbc2074 | ||
|
|
1a8b6a7398 | ||
|
|
e5cf042ea9 | ||
|
|
c435f84afc | ||
|
|
de0db4d014 | ||
|
|
0afcf6d86a | ||
|
|
e0e3afa5b9 | ||
|
|
94d8f72904 | ||
|
|
9b9777feaf | ||
|
|
0c2e37e127 | ||
|
|
10ae60f79b | ||
|
|
fbb34ba4b3 | ||
|
|
5ee827ecaf | ||
|
|
dc3ffb3bcb | ||
|
|
4affefad88 | ||
|
|
344e6ad2fd | ||
|
|
fd57d32bb5 | ||
|
|
0581950b21 | ||
|
|
a07bb4ac60 | ||
|
|
eab7a64250 | ||
|
|
98244b9572 | ||
|
|
2c156e9a99 | ||
|
|
d33df04f38 | ||
|
|
9c196b99c9 | ||
|
|
9fcf4ec19f | ||
|
|
c3e39311c1 | ||
|
|
e7d928a065 | ||
|
|
608ec03047 | ||
|
|
92359ebaed | ||
|
|
c19f64144a | ||
|
|
8642ac0946 | ||
|
|
dee5842c9c | ||
|
|
a6deeef717 | ||
|
|
0160e8eeeb | ||
|
|
f519cbaf1e | ||
|
|
ef1ce4a389 | ||
|
|
c8f6502b02 | ||
|
|
afd93c34a2 | ||
|
|
d52715a8fa | ||
|
|
549500887f | ||
|
|
40947e39e8 |
14
.github/workflows/windows.yaml
vendored
14
.github/workflows/windows.yaml
vendored
@@ -73,6 +73,11 @@ jobs:
|
||||
- name: Install the target
|
||||
run: |
|
||||
rustup target install ${{ matrix.target }}
|
||||
- name: Run Cargo checks
|
||||
run: |
|
||||
cargo fmt --check
|
||||
cargo check
|
||||
cargo clippy
|
||||
- name: Run a full build
|
||||
run: |
|
||||
cargo build --locked --release --target ${{ matrix.target }}
|
||||
@@ -93,6 +98,11 @@ jobs:
|
||||
target/${{ matrix.target }}/release/komorebic-no-console.pdb
|
||||
target/wix/komorebi-*.msi
|
||||
retention-days: 7
|
||||
- name: Check GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: build --skip=validate --clean
|
||||
|
||||
# Release
|
||||
- name: Generate changelog
|
||||
@@ -106,12 +116,12 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
version: latest
|
||||
args: release --skip-validate --clean --release-notes=CHANGELOG.md
|
||||
args: release --skip=validate --clean --release-notes=CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
|
||||
- name: Add MSI to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
files: "target/wix/komorebi-*.msi"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@
|
||||
/target
|
||||
CHANGELOG.md
|
||||
dummy.go
|
||||
komorebi.ahk
|
||||
komorebi.ahk
|
||||
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,8 +28,8 @@ 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:
|
||||
@@ -40,7 +40,7 @@ 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.
|
||||
```
|
||||
1073
Cargo.lock
generated
1073
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -4,20 +4,25 @@ resolver = "2"
|
||||
members = [
|
||||
"derive-ahk",
|
||||
"komorebi",
|
||||
"komorebi-client",
|
||||
"komorebi-core",
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
windows-interface = { version = "0.52" }
|
||||
windows-implement = { version = "0.52" }
|
||||
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" }
|
||||
sysinfo = "0.30"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
uds_windows = "1"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.52"
|
||||
version = "0.54"
|
||||
features = [
|
||||
"implement",
|
||||
"Win32_System_Com",
|
||||
|
||||
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.
|
||||
|
||||
229
README.md
229
README.md
@@ -82,6 +82,21 @@ A [detailed installation and quickstart
|
||||
guide](https://lgug2z.github.io/komorebi/installation.html) is available which shows how to get started
|
||||
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
|
||||
|
||||
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
|
||||
@@ -99,20 +114,90 @@ widget enabled. The original video can be viewed
|
||||
|
||||
https://user-images.githubusercontent.com/13164844/163496414-a9cde3d1-b8a7-4a7a-96fb-a8985380bc70.mp4
|
||||
|
||||
# Development
|
||||
# Contribution Guidelines
|
||||
|
||||
If you would like to contribute code to this repository, there are a few requests that I have to ensure a foundation of
|
||||
code quality, consistency and commit hygiene:
|
||||
If you would like to contribute to `komorebi` please take the time to carefully read the guidelines below.
|
||||
|
||||
## Commit hygiene
|
||||
|
||||
- Flatten all `use` statements
|
||||
- Run `cargo +nightly clippy` and ensure that all lints and suggestions have been addressed before committing
|
||||
- Run `cargo +stable clippy` and ensure that all lints and suggestions have been addressed before committing
|
||||
- Run `cargo +nightly fmt --all` to ensure consistent formatting before committing
|
||||
- Use `git cz` with
|
||||
the [Commitizen CLI](https://github.com/commitizen/cz-cli#conventional-commit-messages-as-a-global-utility) to prepare
|
||||
commit messages
|
||||
- Provide at least one short sentence or paragraph in your commit message body to describe your thought process for the
|
||||
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for the
|
||||
changes being committed
|
||||
|
||||
## PRs should contain only a single feature or bug fix
|
||||
|
||||
It is very difficult to review pull requests which touch multiple unrelated features and parts of the codebase.
|
||||
|
||||
Please do not submit pull requests like this; you will be asked to separate them into smaller PRs that deal only with
|
||||
one feature or bug fix at a time.
|
||||
|
||||
If you are working on multiple features and bug fixes, I suggest that you cut a branch called `local-trunk`
|
||||
from `master` which you keep up to date, and rebase the various independent branches you are working on onto that branch
|
||||
if you want to test them together or create a build with everything integrated.
|
||||
|
||||
## Refactors to the codebase must have prior approval
|
||||
|
||||
`komorebi` is a mature codebase with an internal consistency and structure that has developed organically over close to
|
||||
half a decade.
|
||||
|
||||
There are [countless hours of live coding videos](https://youtube.com/@LGUG2Z) demonstrating work on this project and
|
||||
showing new contributors how to do everything from basic tasks like implementing new `komorebic` commands to
|
||||
distinguishing monitors by manufacturer hardware identifiers and video card ports.
|
||||
|
||||
Refactors to the structure of the codebase are not taken lightly and require prior discussion and approval.
|
||||
|
||||
Please do not start refactoring the codebase with the expectation of having your changes integrated until you receive an
|
||||
explicit approval or a request to do so.
|
||||
|
||||
Similarly, when implementing features and bug fixes, please stick to the structure of the codebase as much as possible
|
||||
and do not take this as an opportunity to do some "refactoring along the way".
|
||||
|
||||
It is extremely difficult to review PRs for features and bug fixes if they are lost in sweeping changes to the structure
|
||||
of the codebase.
|
||||
|
||||
## Breaking changes to user-facing interfaces are unacceptable
|
||||
|
||||
This includes but is not limited to:
|
||||
|
||||
- All `komorebic` commands
|
||||
- The `komorebi.json` schema
|
||||
- The [`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
schema
|
||||
|
||||
No user should ever find that their configuration file has stopped working after upgrading to a new version
|
||||
of `komorebi`.
|
||||
|
||||
More often than not there are ways to reformulate changes that may initially seem like they require breaking user-facing
|
||||
interfaces into additive changes.
|
||||
|
||||
For some inspiration please take a look
|
||||
at [this commit](https://github.com/LGUG2Z/komorebi/commit/e7d928a065eb63bb4ea1fb864c69c1cae8cc763b) which added the
|
||||
ability for users to specify colours in `komorebi.json` in Hex format alongside RGB.
|
||||
|
||||
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
|
||||
the IDE for completions and navigation:
|
||||
|
||||
@@ -151,20 +236,21 @@ found, information about it will appear in the log which can be shared when open
|
||||
# Window Manager State and Integrations
|
||||
|
||||
The current state of the window manager can be queried using the `komorebic state` command, which returns a JSON
|
||||
representation of the `State` struct, which includes the current state of `WindowManager`.
|
||||
representation of the `State` struct.
|
||||
|
||||
This may also be polled to build further integrations and widgets on top of (if you ever wanted to build something
|
||||
like [Stackline](https://github.com/AdamWagner/stackline) for Windows, you could do it by polling this command).
|
||||
This may also be polled to build further integrations and widgets on top of.
|
||||
|
||||
# Window Manager Event Subscriptions
|
||||
|
||||
It is also possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
|
||||
## Named Pipes
|
||||
|
||||
It is possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
|
||||
by `komorebi` using [Named Pipes](https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes).
|
||||
|
||||
First, your application must create a named pipe. Once the named pipe has been created, run the following command:
|
||||
|
||||
```powershell
|
||||
komorebic.exe subscribe <your pipe name>
|
||||
komorebic.exe subscribe-pipe <your pipe name>
|
||||
```
|
||||
|
||||
Note that you do not have to include the full path of the named pipe, just the name.
|
||||
@@ -188,12 +274,125 @@ You may then filter on the `type` key to listen to the events that you are inter
|
||||
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
|
||||
in `komorebi-core`.
|
||||
|
||||
An example of how to create a named pipe and a subscription to `komorebi`'s handled events in Python
|
||||
by [@denBot](https://github.com/denBot) can be
|
||||
found [here](https://gist.github.com/denBot/4136279812f87819f86d99eba77c1ee0).
|
||||
Below is an example of how you can subscribe to and filter on events using a named pipe in `nodejs`.
|
||||
|
||||
An example of how to create a named pipe and a subscription to `komorebi`'s handled events in Rust can also be found
|
||||
in the [`komokana`](https://github.com/LGUG2Z/komokana) repository.
|
||||
```javascript
|
||||
const { exec } = require("child_process");
|
||||
const net = require("net");
|
||||
|
||||
const pipeName = "\\\\.\\pipe\\komorebi-js";
|
||||
const server = net.createServer((stream) => {
|
||||
console.log("Client connected");
|
||||
|
||||
// Every time there is a workspace-related event, let's log the names of all
|
||||
// workspaces on the currently focused monitor, and then log the name of the
|
||||
// currently focused workspace on that monitor
|
||||
|
||||
stream.on("data", (data) => {
|
||||
let json = JSON.parse(data.toString());
|
||||
let event = json.event;
|
||||
|
||||
if (event.type.includes("Workspace")) {
|
||||
let monitors = json.state.monitors;
|
||||
let current_monitor = monitors.elements[monitors.focused];
|
||||
let workspaces = monitors.elements[monitors.focused].workspaces;
|
||||
let current_workspace = workspaces.elements[workspaces.focused];
|
||||
|
||||
console.log(
|
||||
workspaces.elements
|
||||
.map((workspace) => workspace.name)
|
||||
.filter((name) => name !== null)
|
||||
);
|
||||
console.log(current_workspace.name);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on("end", () => {
|
||||
console.log("Client disconnected");
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(pipeName, () => {
|
||||
console.log("Named pipe server listening");
|
||||
});
|
||||
|
||||
const command = "komorebic subscribe-pipe komorebi-js";
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error executing command: ${error}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Unix Domain Sockets
|
||||
|
||||
It is possible to subscribe to notifications of every `WindowManagerEvent` and `SocketMessage` handled
|
||||
by `komorebi` using [Unix Domain Sockets](https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/).
|
||||
|
||||
UDS are also the only mode of communication between `komorebi` and `komorebic`.
|
||||
|
||||
First, your application must create a socket in `$ENV:LocalAppData\komorebi`. Once the socket has been created, run the
|
||||
following command:
|
||||
|
||||
```powershell
|
||||
komorebic.exe subscribe-socket <your socket name>
|
||||
```
|
||||
|
||||
If the socket exists, komorebi will start pushing JSON data of successfully handled events and messages as in the
|
||||
example above in the Named Pipes section.
|
||||
|
||||
## Rust Client
|
||||
|
||||
As of `v0.1.22` it is possible to use the `komorebi-client` crate to subscribe to notifications of
|
||||
every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust codebase.
|
||||
|
||||
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.25"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::UnixListener;
|
||||
use komorebi_client::WindowManagerEvent;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
let socket = komorebi_client::subscribe(NAME)?;
|
||||
|
||||
for incoming in socket.incoming() {
|
||||
match incoming {
|
||||
Ok(data) => {
|
||||
let reader = BufReader::new(data.try_clone()?);
|
||||
|
||||
for line in reader.lines().flatten() {
|
||||
let notification: Notification = match serde_json::from_str(&line) {
|
||||
Ok(notification) => notification,
|
||||
Err(error) => {
|
||||
log::debug!("discarding malformed komorebi notification: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// match and filter on desired notifications
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
log::debug!("{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
A read-world example can be found
|
||||
in [komokana](https://github.com/LGUG2Z/komokana/blob/feature/komorebi-uds/src/main.rs).
|
||||
|
||||
## Subscription Event Notification Schema
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# alt-focus-hack
|
||||
|
||||
```
|
||||
Enable or disable a hack simulating ALT key presses to ensure focus changes succeed
|
||||
|
||||
Usage: komorebic.exe alt-focus-hack <BOOLEAN_STATE>
|
||||
|
||||
Arguments:
|
||||
<BOOLEAN_STATE>
|
||||
[possible values: enable, disable]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -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]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# check
|
||||
|
||||
```
|
||||
Output various important komorebi-related environment values
|
||||
Check komorebi configuration and related files for common errors
|
||||
|
||||
Usage: komorebic.exe check
|
||||
|
||||
|
||||
12
docs/cli/configuration.md
Normal file
12
docs/cli/configuration.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# configuration
|
||||
|
||||
```
|
||||
Show the path to komorebi.json
|
||||
|
||||
Usage: komorebic.exe configuration
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
# docgen
|
||||
|
||||
```
|
||||
Usage: komorebic.exe docgen
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe float-rule <IDENTIFIER> <ID>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
[possible values: exe, class, title, path]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
# identify-border-overflow-application
|
||||
|
||||
```
|
||||
Identify an application that has overflowing borders
|
||||
|
||||
Usage: komorebic.exe identify-border-overflow-application <IDENTIFIER> <ID>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe identify-layered-application <IDENTIFIER> <ID>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
[possible values: exe, class, title, path]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe identify-object-name-change-application <IDENTIFIER> <ID>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
[possible values: exe, class, title, path]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe identify-tray-application <IDENTIFIER> <ID>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
[possible values: exe, class, title, path]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe initial-named-workspace-rule <IDENTIFIER> <ID> <WORKSPACE>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
[possible values: exe, class, title, path]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe initial-workspace-rule <IDENTIFIER> <ID> <MONITOR> <WORKSPA
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
[possible values: exe, class, title, path]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe manage-rule <IDENTIFIER> <ID>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
[possible values: exe, class, title, path]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
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]
|
||||
[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]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe named-workspace-rule <IDENTIFIER> <ID> <WORKSPACE>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
[possible values: exe, class, title, path]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe remove-title-bar <IDENTIFIER> <ID>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
[possible values: exe, class, title, path]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# subscribe
|
||||
# subscribe-pipe
|
||||
|
||||
```
|
||||
Subscribe to komorebi events
|
||||
Subscribe to komorebi events using a Named Pipe
|
||||
|
||||
Usage: komorebic.exe subscribe <NAMED_PIPE>
|
||||
Usage: komorebic.exe subscribe-pipe <NAMED_PIPE>
|
||||
|
||||
Arguments:
|
||||
<NAMED_PIPE>
|
||||
16
docs/cli/subscribe-socket.md
Normal file
16
docs/cli/subscribe-socket.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# subscribe-socket
|
||||
|
||||
```
|
||||
Subscribe to komorebi events using a Unix Domain Socket
|
||||
|
||||
Usage: komorebic.exe subscribe-socket <SOCKET>
|
||||
|
||||
Arguments:
|
||||
<SOCKET>
|
||||
Name of the socket to send event notifications to
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,9 +1,9 @@
|
||||
# unsubscribe
|
||||
# unsubscribe-pipe
|
||||
|
||||
```
|
||||
Unsubscribe from komorebi events
|
||||
|
||||
Usage: komorebic.exe unsubscribe <NAMED_PIPE>
|
||||
Usage: komorebic.exe unsubscribe-pipe <NAMED_PIPE>
|
||||
|
||||
Arguments:
|
||||
<NAMED_PIPE>
|
||||
16
docs/cli/unsubscribe-socket.md
Normal file
16
docs/cli/unsubscribe-socket.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# unsubscribe-socket
|
||||
|
||||
```
|
||||
Unsubscribe from komorebi events
|
||||
|
||||
Usage: komorebic.exe unsubscribe-socket <SOCKET>
|
||||
|
||||
Arguments:
|
||||
<SOCKET>
|
||||
Name of the socket to stop sending event notifications to
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/whkdrc.md
Normal file
12
docs/cli/whkdrc.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# whkdrc
|
||||
|
||||
```
|
||||
Show the path to whkdrc
|
||||
|
||||
Usage: komorebic.exe whkdrc
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print 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]
|
||||
[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]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe workspace-rule <IDENTIFIER> <ID> <MONITOR> <WORKSPACE>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
[possible values: exe, class, title]
|
||||
[possible values: exe, class, title, path]
|
||||
|
||||
<ID>
|
||||
Identifier as a string
|
||||
|
||||
@@ -7,6 +7,7 @@ file.
|
||||
```json
|
||||
{
|
||||
"active_window_border": true,
|
||||
"active_window_border_style": "Rounded",
|
||||
"active_window_border_colours": {
|
||||
"single": {
|
||||
"r": 66,
|
||||
@@ -23,7 +24,7 @@ file.
|
||||
"g": 51,
|
||||
"b": 153
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
```
|
||||
@@ -31,10 +32,8 @@ file.
|
||||
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
|
||||
This feature is not considered stable, and you may encounter visual artifacts
|
||||
from time to time.
|
||||
|
||||
<!-- TODO: Record a new video -->
|
||||
|
||||
[](https://www.youtube.com/watch?v=ywiAvoMV_gE)
|
||||
video](https://img.youtube.com/vi/7_9D22t7KK4/hqdefault.jpg)](https://www.youtube.com/watch?v=7_9D22t7KK4)
|
||||
|
||||
@@ -21,9 +21,6 @@ hotkey bindings.
|
||||
# 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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,3 +25,6 @@ If you already have configuration files that you wish to keep, move them to the
|
||||
|
||||
The next time you run `komorebic start`, any files created by or loaded by
|
||||
_komorebi_ will be placed or expected to exist in this folder.
|
||||
|
||||
[](https://www.youtube.com/watch?v=C_KWUqQ6kko)
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
|
||||
If you would like to remove all gaps by default, both between windows
|
||||
themselves, and between the monitor edges and the windows, you can set the
|
||||
following two configuration options to `0` in the `komorebi.json` configuration
|
||||
file.
|
||||
following configuration options to `0` and `-1` in the `komorebi.json`
|
||||
configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"default_workspace_padding": 0,
|
||||
"default_container_padding": 0
|
||||
"default_container_padding": 0,
|
||||
"border_width": 0,
|
||||
"border_offset": -1
|
||||
}
|
||||
```
|
||||
|
||||
<!-- TODO: Record a new video -->
|
||||
A restart of `komorebi` is required after changing these settings.
|
||||
|
||||
[](https://www.youtube.com/watch?v=eGr07mymgWE)
|
||||
[](https://www.youtube.com/watch?v=6QYLao953XE)
|
||||
|
||||
23
docs/common-workflows/stackbar.md
Normal file
23
docs/common-workflows/stackbar.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Stackbar
|
||||
|
||||
If you would like to add a visual stackbar to show which windows are in a container
|
||||
stack ensure the following options are defined in the `komorebi.json` configuration
|
||||
file.
|
||||
|
||||
```json
|
||||
{
|
||||
"stackbar": {
|
||||
"height": 40,
|
||||
"mode": "OnStack",
|
||||
"tabs": {
|
||||
"width": 300,
|
||||
"focused_text": "#00a542",
|
||||
"unfocused_text": "#b3b3b3",
|
||||
"background": "#141414"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This feature is not considered stable, and you may encounter visual artifacts
|
||||
from time to time.
|
||||
@@ -16,6 +16,12 @@ the example files have been downloaded. For most new users this will be in the
|
||||
komorebic quickstart
|
||||
```
|
||||
|
||||
With the example configurations downloaded, you can now start `komorebi` and `whkd.
|
||||
|
||||
```powershell
|
||||
komorebic start --whkd
|
||||
```
|
||||
|
||||
## komorebi.json
|
||||
|
||||
The example window manager configuration sets some sane defaults and provides
|
||||
@@ -95,6 +101,16 @@ monocle.
|
||||
+-------+-----+
|
||||
```
|
||||
|
||||
#### RightMainVerticalStack
|
||||
|
||||
```
|
||||
+-----+-------+
|
||||
| | |
|
||||
+-----+ |
|
||||
| | |
|
||||
+-----+-------+
|
||||
```
|
||||
|
||||
#### Horizontal Stack
|
||||
|
||||
```
|
||||
@@ -116,6 +132,7 @@ monocle.
|
||||
```
|
||||
|
||||
#### Rows
|
||||
|
||||
If you have a vertical monitor, I recommend using this layout.
|
||||
|
||||
```
|
||||
@@ -127,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.
|
||||
|
||||
```
|
||||
@@ -139,12 +157,26 @@ 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!
|
||||
|
||||
```
|
||||
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||
| | | | | | | | | | | | | | |
|
||||
| | | | | | | | | | | | | +---+
|
||||
+-----+-----+ | +---+---+ +---+---+---+ +---+---| |
|
||||
| | | | | | | | | | | | | +---+
|
||||
| | | | | | | | | | | | | | |
|
||||
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||
4 windows 5 windows 6 windows 7 windows
|
||||
```
|
||||
|
||||
## whkdrc
|
||||
|
||||
`whkd` is a fairly basic piece of software with a simple configuration format:
|
||||
key bindings go to the left of the, and shell commands go to the right of the
|
||||
colon.
|
||||
key bindings go to the left of the colon, and shell commands go to the right of the
|
||||
colon. By default, the `whkdrc` file should be located in the `$Env:USERPROFILE/.config/` directory.
|
||||
|
||||
Please remember that `whkd` does not support overriding Microsoft's limitations
|
||||
on hotkey bindings that include the `Windows` key. If this is important to you,
|
||||
@@ -164,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
|
||||
|
||||
@@ -33,6 +33,7 @@ it from [source](https://github.com/LGUG2Z/komorebi) if you would prefer.
|
||||
- [Scoop](#scoop)
|
||||
- [WinGet](#winget)
|
||||
- [Building from source](#building-from-source)
|
||||
- [Offline](#offline)
|
||||
|
||||
## Long path support
|
||||
|
||||
@@ -111,3 +112,12 @@ cargo +stable install --path komorebic-no-console --locked
|
||||
|
||||
If the binaries have been built and added to your `$PATH` correctly, you should
|
||||
see some output when running `komorebi --help` and `komorebic --help`
|
||||
|
||||
### Offline
|
||||
|
||||
Download the latest [komorebi](https://github.com/LGUG2Z/komorebi/releases)
|
||||
and [whkd](https://github.com/LGUG2Z/whkd/releases) MSI installers on an internet-connected computer, then copy them to
|
||||
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).
|
||||
|
||||
@@ -1,24 +1,59 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.25/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_width": 8,
|
||||
"border_offset": -1,
|
||||
"active_window_border": false,
|
||||
"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 }
|
||||
"single": "#42a5f5",
|
||||
"stack": "#00a542",
|
||||
"monocle": "#ff3399"
|
||||
},
|
||||
"stackbar": {
|
||||
"height": 40,
|
||||
"mode": "Never",
|
||||
"tabs": {
|
||||
"width": 300,
|
||||
"focused_text": "#00a542",
|
||||
"unfocused_text": "#b3b3b3",
|
||||
"background": "#141414"
|
||||
}
|
||||
},
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{ "name": "I", "layout": "BSP" },
|
||||
{ "name": "II", "layout": "VerticalStack" },
|
||||
{ "name": "III", "layout": "HorizontalStack" },
|
||||
{ "name": "IV", "layout": "UltrawideVerticalStack" },
|
||||
{ "name": "V", "layout": "Rows" }
|
||||
{
|
||||
"name": "I",
|
||||
"layout": "BSP"
|
||||
},
|
||||
{
|
||||
"name": "II",
|
||||
"layout": "VerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "III",
|
||||
"layout": "HorizontalStack"
|
||||
},
|
||||
{
|
||||
"name": "IV",
|
||||
"layout": "UltrawideVerticalStack"
|
||||
},
|
||||
{
|
||||
"name": "V",
|
||||
"layout": "Rows"
|
||||
},
|
||||
{
|
||||
"name": "VI",
|
||||
"layout": "Grid"
|
||||
},
|
||||
{
|
||||
"name": "VII",
|
||||
"layout": "RightMainVerticalStack"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
56
docs/release/v0-1-22.md
Normal file
56
docs/release/v0-1-22.md
Normal file
@@ -0,0 +1,56 @@
|
||||
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.
|
||||
|
||||
## tl;dr
|
||||
|
||||
The way windows are sized and drawn has been improved to remove the need to manually specify and remove invisible
|
||||
borders for applications that overflow them. If you use the active window border, the first time you launch `v0.1.22`
|
||||
you may end up with a _huge_ border due to these changes.
|
||||
|
||||
`active_window_border_width` and `active_window_border_offset` have been renamed to `border_width` and `border_offset`
|
||||
as they now also apply outside the context of the active window border.
|
||||
|
||||
```json
|
||||
{
|
||||
"active_window_border": true,
|
||||
"border_width": 8,
|
||||
"border_offset": -1
|
||||
}
|
||||
```
|
||||
|
||||
Users of the active window border should start from these settings and read the notes below before making further
|
||||
adjustments.
|
||||
|
||||
## Changes to `active_window_border`, and window sizing:
|
||||
|
||||
- The border no longer creates a second drop-shadow around the active window
|
||||
- Windows are now sized to fill the layout region entirely, ignoring window decorations such as drop shadows
|
||||
- Border offset now starts exactly at the paint edge of the window on all sides
|
||||
- Windows are sized such that the border offset and border width are taken into account
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
### Gapless
|
||||
|
||||
- Disable "transparency effects" Personalization > Colors
|
||||
- Set the following settings in `komorebi.json`:
|
||||
```json
|
||||
{
|
||||
"default_workspace_padding": 0,
|
||||
"default_container_padding": 0,
|
||||
"border_offset": -1,
|
||||
"border_width": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 1px border
|
||||
|
||||
A 1px border is drawn around the window edge. Users may see a gap for a single pixel, if the system theme has a
|
||||
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.
|
||||
6
justfile
6
justfile
@@ -28,7 +28,6 @@ install:
|
||||
just install-target komorebi
|
||||
|
||||
run:
|
||||
just install-target komorebic
|
||||
cargo +stable run --bin komorebi --locked
|
||||
|
||||
warn $RUST_LOG="warn":
|
||||
@@ -44,17 +43,12 @@ trace $RUST_LOG="trace":
|
||||
just run
|
||||
|
||||
deadlock $RUST_LOG="trace":
|
||||
just install-komorebic
|
||||
cargo +stable run --bin komorebi --locked --features deadlock_detection
|
||||
|
||||
docgen:
|
||||
komorebic docgen
|
||||
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
|
||||
|
||||
exampledocs:
|
||||
cp whkdrc.sample docs/whkdrc.sample
|
||||
cp komorebi.example.json docs/komorebi.example.json
|
||||
|
||||
schemagen:
|
||||
komorebic static-config-schema > schema.json
|
||||
generate-schema-doc .\schema.json --config template_name=js_offline --config minify=false .\static-config-docs\
|
||||
|
||||
12
komorebi-client/Cargo.toml
Normal file
12
komorebi-client/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.26-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi = { path = "../komorebi" }
|
||||
komorebi-core = { path = "../komorebi-core" }
|
||||
uds_windows = "1"
|
||||
serde_json = { workspace = true }
|
||||
90
komorebi-client/src/lib.rs
Normal file
90
komorebi-client/src/lib.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![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::ActiveWindowBorderColours;
|
||||
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::ActiveWindowBorderStyle;
|
||||
pub use komorebi_core::Arrangement;
|
||||
pub use komorebi_core::Axis;
|
||||
pub use komorebi_core::CustomLayout;
|
||||
pub use komorebi_core::CycleDirection;
|
||||
pub use komorebi_core::DefaultLayout;
|
||||
pub use komorebi_core::Direction;
|
||||
pub use komorebi_core::Layout;
|
||||
pub use komorebi_core::OperationDirection;
|
||||
pub use komorebi_core::Rect;
|
||||
pub use komorebi_core::SocketMessage;
|
||||
pub use komorebi_core::StackbarMode;
|
||||
pub use komorebi_core::WindowKind;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::Shutdown;
|
||||
pub use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
const KOMOREBI: &str = "komorebi.sock";
|
||||
|
||||
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut response = String::new();
|
||||
reader.read_to_string(&mut response)?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
|
||||
let socket = DATA_DIR.join(name);
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(()) => {}
|
||||
Err(error) => match error.kind() {
|
||||
std::io::ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
|
||||
send_message(&SocketMessage::AddSubscriberSocket(name.to_string()))?;
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.21"
|
||||
version = "0.1.26-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -8,7 +8,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
|
||||
@@ -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,14 +267,214 @@ 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
|
||||
}
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
Self::Grid => {
|
||||
// Shamelessly lifted from LeftWM
|
||||
// https://github.com/leftwm/leftwm/blob/18675067b8450e520ef75db2ebbb0d973aa1199e/leftwm-core/src/layouts/grid_horizontal.rs
|
||||
let mut layouts: Vec<Rect> = vec![];
|
||||
layouts.resize(len, Rect::default());
|
||||
|
||||
let len = len as i32;
|
||||
|
||||
let num_cols = (len as f32).sqrt().ceil() as i32;
|
||||
let mut iter = layouts.iter_mut().enumerate().peekable();
|
||||
|
||||
for col in 0..num_cols {
|
||||
let iter_peek = iter.peek().map(|x| x.0).unwrap_or_default() as i32;
|
||||
let remaining_windows = len - iter_peek;
|
||||
let remaining_columns = num_cols - col;
|
||||
let num_rows_in_this_col = remaining_windows / remaining_columns;
|
||||
|
||||
let win_height = area.bottom / num_rows_in_this_col;
|
||||
let win_width = area.right / num_cols;
|
||||
|
||||
for row in 0..num_rows_in_this_col {
|
||||
if let Some((_idx, win)) = iter.next() {
|
||||
let mut left = area.left + win_width * col;
|
||||
let mut top = area.top + win_height * row;
|
||||
|
||||
match layout_flip {
|
||||
Some(Axis::Horizontal) => {
|
||||
left = area.right - win_width * (col + 1) + area.left;
|
||||
}
|
||||
Some(Axis::Vertical) => {
|
||||
top = area.bottom - win_height * (row + 1) + area.top;
|
||||
}
|
||||
Some(Axis::HorizontalAndVertical) => {
|
||||
left = area.right - win_width * (col + 1) + area.left;
|
||||
top = area.bottom - win_height * (row + 1) + area.top;
|
||||
}
|
||||
None => {} // No flip
|
||||
}
|
||||
|
||||
win.bottom = win_height;
|
||||
win.right = win_width;
|
||||
win.left = left;
|
||||
win.top = top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::UltrawideVerticalStack => ultrawide(area, len, layout_flip, resize_dimensions),
|
||||
};
|
||||
|
||||
dimensions
|
||||
.iter_mut()
|
||||
.for_each(|l| l.add_padding(container_padding));
|
||||
.for_each(|l| l.add_padding(container_padding.unwrap_or_default()));
|
||||
|
||||
dimensions
|
||||
}
|
||||
@@ -258,7 +597,7 @@ impl Arrangement for CustomLayout {
|
||||
|
||||
dimensions
|
||||
.iter_mut()
|
||||
.for_each(|l| l.add_padding(container_padding));
|
||||
.for_each(|l| l.add_padding(container_padding.unwrap_or_default()));
|
||||
|
||||
dimensions
|
||||
}
|
||||
@@ -267,7 +606,6 @@ impl Arrangement for CustomLayout {
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Axis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
@@ -316,6 +654,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();
|
||||
|
||||
@@ -509,6 +863,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];
|
||||
@@ -599,99 +1134,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
|
||||
}
|
||||
|
||||
@@ -8,15 +8,17 @@ use strum::EnumString;
|
||||
|
||||
use crate::ApplicationIdentifier;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema)]
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ApplicationOptions {
|
||||
ObjectNameChange,
|
||||
Layered,
|
||||
BorderOverflow,
|
||||
TrayAndMultiWindow,
|
||||
Force,
|
||||
BorderOverflow,
|
||||
}
|
||||
|
||||
impl ApplicationOptions {
|
||||
@@ -29,15 +31,15 @@ impl ApplicationOptions {
|
||||
ApplicationOptions::Layered => {
|
||||
format!("komorebic.exe identify-layered-application {kind} \"{id}\"",)
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {
|
||||
format!("komorebic.exe identify-border-overflow-application {kind} \"{id}\"",)
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
format!("komorebic.exe identify-tray-application {kind} \"{id}\"",)
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
format!("komorebic.exe manage-rule {kind} \"{id}\"")
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {
|
||||
unreachable!("deprecated");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +52,13 @@ impl ApplicationOptions {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum MatchingRule {
|
||||
Simple(IdWithIdentifier),
|
||||
Composite(Vec<IdWithIdentifier>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct IdWithIdentifier {
|
||||
pub kind: ApplicationIdentifier,
|
||||
@@ -66,6 +75,10 @@ pub enum MatchingStrategy {
|
||||
EndsWith,
|
||||
Contains,
|
||||
Regex,
|
||||
DoesNotEndWith,
|
||||
DoesNotStartWith,
|
||||
DoesNotEqual,
|
||||
DoesNotContain,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -95,14 +108,14 @@ pub struct ApplicationConfiguration {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub options: Option<Vec<ApplicationOptions>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_identifiers: Option<Vec<IdWithIdentifierAndComment>>,
|
||||
pub float_identifiers: Option<Vec<MatchingRule>>,
|
||||
}
|
||||
|
||||
impl ApplicationConfiguration {
|
||||
pub fn populate_default_matching_strategies(&mut self) {
|
||||
if self.identifier.matching_strategy.is_none() {
|
||||
match self.identifier.kind {
|
||||
ApplicationIdentifier::Exe => {
|
||||
ApplicationIdentifier::Exe | ApplicationIdentifier::Path => {
|
||||
self.identifier.matching_strategy = Option::from(MatchingStrategy::Equals);
|
||||
}
|
||||
ApplicationIdentifier::Class | ApplicationIdentifier::Title => {}
|
||||
@@ -181,19 +194,21 @@ impl ApplicationConfigurationGenerator {
|
||||
}
|
||||
|
||||
if let Some(float_identifiers) = app.float_identifiers {
|
||||
for float in float_identifiers {
|
||||
let float_rule =
|
||||
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
|
||||
for matching_rule in float_identifiers {
|
||||
if let MatchingRule::Simple(float) = matching_rule {
|
||||
let float_rule =
|
||||
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
|
||||
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
|
||||
if let Some(comment) = float.comment {
|
||||
lines.push(format!("# {comment}"));
|
||||
};
|
||||
// if let Some(comment) = float.comment {
|
||||
// lines.push(format!("# {comment}"));
|
||||
// };
|
||||
|
||||
lines.push(float_rule);
|
||||
lines.push(float_rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,21 +245,23 @@ impl ApplicationConfigurationGenerator {
|
||||
}
|
||||
|
||||
if let Some(float_identifiers) = app.float_identifiers {
|
||||
for float in float_identifiers {
|
||||
let float_rule = format!(
|
||||
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
|
||||
float.kind, float.id
|
||||
);
|
||||
for matching_rule in float_identifiers {
|
||||
if let MatchingRule::Simple(float) = matching_rule {
|
||||
let float_rule = format!(
|
||||
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
|
||||
float.kind, float.id
|
||||
);
|
||||
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
|
||||
if let Some(comment) = float.comment {
|
||||
lines.push(format!("; {comment}"));
|
||||
};
|
||||
// if let Some(comment) = float.comment {
|
||||
// lines.push(format!("; {comment}"));
|
||||
// };
|
||||
|
||||
lines.push(float_rule);
|
||||
lines.push(float_rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -20,6 +29,8 @@ pub enum DefaultLayout {
|
||||
VerticalStack,
|
||||
HorizontalStack,
|
||||
UltrawideVerticalStack,
|
||||
Grid,
|
||||
RightMainVerticalStack,
|
||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
||||
}
|
||||
|
||||
@@ -34,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;
|
||||
};
|
||||
|
||||
@@ -135,7 +155,9 @@ impl DefaultLayout {
|
||||
Self::Rows => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::BSP,
|
||||
Self::UltrawideVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +169,9 @@ impl DefaultLayout {
|
||||
Self::HorizontalStack => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::Rows,
|
||||
Self::Rows => Self::Columns,
|
||||
Self::Columns => Self::BSP,
|
||||
Self::Columns => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,30 @@ pub trait Direction {
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> bool;
|
||||
fn up_index(&self, idx: usize) -> usize;
|
||||
fn down_index(&self, idx: usize) -> usize;
|
||||
fn left_index(&self, idx: usize) -> usize;
|
||||
fn right_index(&self, idx: usize) -> usize;
|
||||
fn up_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize;
|
||||
fn down_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize;
|
||||
fn left_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize;
|
||||
fn right_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize;
|
||||
}
|
||||
|
||||
impl Direction for DefaultLayout {
|
||||
@@ -35,28 +55,28 @@ impl Direction for DefaultLayout {
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(idx))
|
||||
Option::from(self.left_index(Some(op_direction), idx, Some(count)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(idx))
|
||||
Option::from(self.right_index(Some(op_direction), idx, Some(count)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(idx))
|
||||
Option::from(self.up_index(Some(op_direction), idx, Some(count)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(idx))
|
||||
Option::from(self.down_index(Some(op_direction), idx, Some(count)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -75,40 +95,51 @@ impl Direction for DefaultLayout {
|
||||
Self::BSP => count > 2 && 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::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::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::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
Self::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
|
||||
Self::Columns => idx != count - 1,
|
||||
Self::Rows => false,
|
||||
Self::VerticalStack => idx == 0,
|
||||
Self::RightMainVerticalStack => idx != 0,
|
||||
Self::HorizontalStack => idx != 0 && idx != count - 1,
|
||||
Self::UltrawideVerticalStack => match count {
|
||||
0 | 1 => false,
|
||||
2 => idx != 0,
|
||||
_ => idx < 2,
|
||||
},
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn up_index(&self, idx: usize) -> usize {
|
||||
fn up_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
@@ -118,20 +149,39 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
fn down_index(&self, idx: usize) -> usize {
|
||||
fn down_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
fn left_index(&self, idx: usize) -> usize {
|
||||
fn left_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
@@ -143,28 +193,152 @@ 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!(),
|
||||
_ => 0,
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
}
|
||||
}
|
||||
|
||||
fn right_index(&self, idx: usize) -> usize {
|
||||
fn right_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize {
|
||||
match self {
|
||||
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,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GridItem {
|
||||
state: GridItemState,
|
||||
row: usize,
|
||||
num_rows: usize,
|
||||
touching_edges: GridTouchingEdges,
|
||||
}
|
||||
|
||||
enum GridItemState {
|
||||
Valid,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct GridTouchingEdges {
|
||||
left: bool,
|
||||
right: bool,
|
||||
up: bool,
|
||||
down: bool,
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn get_grid_item(idx: usize, count: usize) -> GridItem {
|
||||
let num_cols = (count as f32).sqrt().ceil() as usize;
|
||||
let mut iter = 0;
|
||||
|
||||
for col in 0..num_cols {
|
||||
let remaining_windows = count - iter;
|
||||
let remaining_columns = num_cols - col;
|
||||
let num_rows_in_this_col = remaining_windows / remaining_columns;
|
||||
|
||||
for row in 0..num_rows_in_this_col {
|
||||
if iter == idx {
|
||||
return GridItem {
|
||||
state: GridItemState::Valid,
|
||||
row: row + 1,
|
||||
num_rows: num_rows_in_this_col,
|
||||
touching_edges: GridTouchingEdges {
|
||||
left: col == 0,
|
||||
right: col == num_cols - 1,
|
||||
up: row == 0,
|
||||
down: row == num_rows_in_this_col - 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
iter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
GridItem {
|
||||
state: GridItemState::Invalid,
|
||||
row: 0,
|
||||
num_rows: 0,
|
||||
touching_edges: GridTouchingEdges {
|
||||
left: true,
|
||||
right: true,
|
||||
up: true,
|
||||
down: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn is_grid_edge(op_direction: OperationDirection, idx: usize, count: usize) -> bool {
|
||||
let item = get_grid_item(idx, count);
|
||||
|
||||
match item.state {
|
||||
GridItemState::Invalid => false,
|
||||
GridItemState::Valid => match op_direction {
|
||||
OperationDirection::Left => item.touching_edges.left,
|
||||
OperationDirection::Right => item.touching_edges.right,
|
||||
OperationDirection::Up => item.touching_edges.up,
|
||||
OperationDirection::Down => item.touching_edges.down,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn grid_neighbor(
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
) -> usize {
|
||||
let Some(op_direction) = op_direction else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let Some(count) = count else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let item = get_grid_item(idx, count);
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
let item_from_prev_col = get_grid_item(idx - item.row, count);
|
||||
|
||||
if item.touching_edges.up && item.num_rows != item_from_prev_col.num_rows {
|
||||
return idx - (item.num_rows - 1);
|
||||
}
|
||||
|
||||
if item.num_rows != item_from_prev_col.num_rows && !item.touching_edges.down {
|
||||
return idx - (item.num_rows - 1);
|
||||
}
|
||||
|
||||
idx - item.num_rows
|
||||
}
|
||||
OperationDirection::Right => idx + item.num_rows,
|
||||
OperationDirection::Up => idx - 1,
|
||||
OperationDirection::Down => idx + 1,
|
||||
}
|
||||
}
|
||||
|
||||
impl Direction for CustomLayout {
|
||||
fn index_in_direction(
|
||||
&self,
|
||||
@@ -179,28 +353,28 @@ impl Direction for CustomLayout {
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(idx))
|
||||
Option::from(self.left_index(None, idx, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(idx))
|
||||
Option::from(self.right_index(None, idx, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(idx))
|
||||
Option::from(self.up_index(None, idx, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(idx))
|
||||
Option::from(self.down_index(None, idx, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -254,15 +428,30 @@ impl Direction for CustomLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fn up_index(&self, idx: usize) -> usize {
|
||||
fn up_index(
|
||||
&self,
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
) -> usize {
|
||||
idx - 1
|
||||
}
|
||||
|
||||
fn down_index(&self, idx: usize) -> usize {
|
||||
fn down_index(
|
||||
&self,
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
) -> usize {
|
||||
idx + 1
|
||||
}
|
||||
|
||||
fn left_index(&self, idx: usize) -> usize {
|
||||
fn left_index(
|
||||
&self,
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
if column_idx - 1 == 0 {
|
||||
0
|
||||
@@ -271,7 +460,12 @@ impl Direction for CustomLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fn right_index(&self, idx: usize) -> usize {
|
||||
fn right_index(
|
||||
&self,
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
self.first_container_idx(column_idx + 1)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ pub enum SocketMessage {
|
||||
SendContainerToWorkspaceNumber(usize),
|
||||
CycleSendContainerToWorkspace(CycleDirection),
|
||||
SendContainerToMonitorWorkspaceNumber(usize, usize),
|
||||
MoveContainerToMonitorWorkspaceNumber(usize, usize),
|
||||
SendContainerToNamedWorkspace(String),
|
||||
MoveWorkspaceToMonitorNumber(usize),
|
||||
SwapWorkspacesToMonitorNumber(usize),
|
||||
@@ -131,9 +132,16 @@ pub enum SocketMessage {
|
||||
AltFocusHack(bool),
|
||||
ActiveWindowBorder(bool),
|
||||
ActiveWindowBorderColour(WindowKind, u32, u32, u32),
|
||||
ActiveWindowBorderWidth(i32),
|
||||
ActiveWindowBorderOffset(i32),
|
||||
ActiveWindowBorderStyle(ActiveWindowBorderStyle),
|
||||
BorderWidth(i32),
|
||||
BorderOffset(i32),
|
||||
InvisibleBorders(Rect),
|
||||
StackbarMode(StackbarMode),
|
||||
StackbarFocusedTextColour(u32, u32, u32),
|
||||
StackbarUnfocusedTextColour(u32, u32, u32),
|
||||
StackbarBackgroundColour(u32, u32, u32),
|
||||
StackbarHeight(i32),
|
||||
StackbarTabWidth(i32),
|
||||
WorkAreaOffset(Rect),
|
||||
MonitorWorkAreaOffset(usize, Rect),
|
||||
ResizeDelta(i32),
|
||||
@@ -148,6 +156,7 @@ pub enum SocketMessage {
|
||||
IdentifyLayeredApplication(ApplicationIdentifier, String),
|
||||
IdentifyBorderOverflowApplication(ApplicationIdentifier, String),
|
||||
State,
|
||||
GlobalState,
|
||||
VisibleWindows,
|
||||
Query(StateQuery),
|
||||
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
|
||||
@@ -156,13 +165,16 @@ pub enum SocketMessage {
|
||||
ToggleMouseFollowsFocus,
|
||||
RemoveTitleBar(ApplicationIdentifier, String),
|
||||
ToggleTitleBars,
|
||||
AddSubscriber(String),
|
||||
RemoveSubscriber(String),
|
||||
AddSubscriberSocket(String),
|
||||
RemoveSubscriberSocket(String),
|
||||
AddSubscriberPipe(String),
|
||||
RemoveSubscriberPipe(String),
|
||||
ApplicationSpecificConfigurationSchema,
|
||||
NotificationSchema,
|
||||
SocketSchema,
|
||||
StaticConfigSchema,
|
||||
GenerateStaticConfig,
|
||||
DebugWindow(isize),
|
||||
}
|
||||
|
||||
impl SocketMessage {
|
||||
@@ -179,20 +191,39 @@ 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,
|
||||
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum WindowKind {
|
||||
Single,
|
||||
Stack,
|
||||
Monocle,
|
||||
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(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
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,
|
||||
@@ -213,7 +244,6 @@ pub enum StateQuery {
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ApplicationIdentifier {
|
||||
#[serde(alias = "exe")]
|
||||
Exe,
|
||||
@@ -221,12 +251,13 @@ pub enum ApplicationIdentifier {
|
||||
Class,
|
||||
#[serde(alias = "title")]
|
||||
Title,
|
||||
#[serde(alias = "path")]
|
||||
Path,
|
||||
}
|
||||
|
||||
#[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,
|
||||
@@ -237,7 +268,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,
|
||||
@@ -248,18 +278,18 @@ 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,
|
||||
@@ -272,7 +302,6 @@ pub enum HidingBehaviour {
|
||||
#[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,
|
||||
@@ -283,7 +312,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,
|
||||
|
||||
@@ -26,9 +26,24 @@ impl From<RECT> for Rect {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for RECT {
|
||||
fn from(rect: Rect) -> Self {
|
||||
Self {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn add_padding(&mut self, padding: Option<i32>) {
|
||||
if let Some(padding) = padding {
|
||||
/// decrease the size of self by the padding amount.
|
||||
pub fn add_padding<T>(&mut self, padding: T)
|
||||
where
|
||||
T: Into<Option<i32>>,
|
||||
{
|
||||
if let Some(padding) = padding.into() {
|
||||
self.left += padding;
|
||||
self.top += padding;
|
||||
self.right -= padding * 2;
|
||||
@@ -36,6 +51,22 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// increase the size of self by the margin amount.
|
||||
pub fn add_margin(&mut self, margin: i32) {
|
||||
self.left -= margin;
|
||||
self.top -= margin;
|
||||
self.right += margin * 2;
|
||||
self.bottom += margin * 2;
|
||||
}
|
||||
|
||||
pub fn left_padding(&mut self, padding: i32) {
|
||||
self.left += padding;
|
||||
}
|
||||
|
||||
pub fn right_padding(&mut self, padding: i32) {
|
||||
self.right -= padding;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn contains_point(&self, point: (i32, i32)) -> bool {
|
||||
point.0 >= self.left
|
||||
@@ -43,4 +74,14 @@ impl Rect {
|
||||
&& point.1 >= self.top
|
||||
&& point.1 <= self.top + self.bottom
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn scale(&self, system_dpi: i32, rect_dpi: i32) -> Rect {
|
||||
Rect {
|
||||
left: (self.left * rect_dpi) / system_dpi,
|
||||
top: (self.top * rect_dpi) / system_dpi,
|
||||
right: (self.right * rect_dpi) / system_dpi,
|
||||
bottom: (self.bottom * rect_dpi) / system_dpi,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/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,
|
||||
"active_window_border": false,
|
||||
"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 }
|
||||
},
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{ "name": "I", "layout": "BSP" },
|
||||
{ "name": "II", "layout": "VerticalStack" },
|
||||
{ "name": "III", "layout": "HorizontalStack" },
|
||||
{ "name": "IV", "layout": "UltrawideVerticalStack" },
|
||||
{ "name": "V", "layout": "Rows" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.21"
|
||||
version = "0.1.26-dev.0"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
@@ -13,39 +13,40 @@ 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"
|
||||
dirs = { workspace = true }
|
||||
getset = "0.1"
|
||||
hotwatch = "0.4"
|
||||
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.7"
|
||||
os_info = "3.8"
|
||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||
paste = "1"
|
||||
regex = "1"
|
||||
schemars = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
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 = "5"
|
||||
which = "6"
|
||||
widestring = "1"
|
||||
windows = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.52"
|
||||
windows-interface = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
widestring = "1"
|
||||
|
||||
[features]
|
||||
deadlock_detection = []
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
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 komorebi_core::Rect;
|
||||
|
||||
use crate::window::should_act;
|
||||
use crate::window::Window;
|
||||
use crate::windows_callbacks;
|
||||
use crate::WindowsApi;
|
||||
use crate::BORDER_HWND;
|
||||
use crate::BORDER_OFFSET;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::BORDER_RECT;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::TRANSPARENCY_COLOUR;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Border {
|
||||
pub(crate) hwnd: isize,
|
||||
}
|
||||
|
||||
impl From<isize> for Border {
|
||||
fn from(hwnd: isize) -> Self {
|
||||
Self { hwnd }
|
||||
}
|
||||
}
|
||||
|
||||
impl Border {
|
||||
pub const fn hwnd(self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn create(name: &str) -> Result<()> {
|
||||
let name: 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);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if *WINDOWS_11 {
|
||||
WindowsApi::round_corners(hwnd.0)?;
|
||||
}
|
||||
|
||||
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,
|
||||
invisible_borders: &Rect,
|
||||
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.top -= invisible_borders.bottom;
|
||||
rect.bottom += invisible_borders.bottom;
|
||||
|
||||
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
let title = &window.title()?;
|
||||
let exe_name = &window.exe()?;
|
||||
let class = &window.class()?;
|
||||
|
||||
let should_expand_border = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
&border_overflows,
|
||||
®ex_identifiers,
|
||||
);
|
||||
|
||||
if should_expand_border {
|
||||
rect.left -= invisible_borders.left;
|
||||
rect.top -= invisible_borders.top;
|
||||
rect.right += invisible_borders.right;
|
||||
rect.bottom += invisible_borders.bottom;
|
||||
}
|
||||
|
||||
let border_offset = BORDER_OFFSET.lock();
|
||||
if let Some(border_offset) = *border_offset {
|
||||
rect.left -= border_offset.left;
|
||||
rect.top -= border_offset.top;
|
||||
rect.right += border_offset.right;
|
||||
rect.bottom += border_offset.bottom;
|
||||
}
|
||||
|
||||
*BORDER_RECT.lock() = rect;
|
||||
|
||||
WindowsApi::position_border_window(self.hwnd(), &rect, activate)
|
||||
}
|
||||
}
|
||||
}
|
||||
224
komorebi/src/border_manager/border.rs
Normal file
224
komorebi/src/border_manager/border.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::FOCUSED;
|
||||
use crate::border_manager::FOCUS_STATE;
|
||||
use crate::border_manager::MONOCLE;
|
||||
use crate::border_manager::RECT_STATE;
|
||||
use crate::border_manager::STACK;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::UNFOCUSED;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
use komorebi_core::ActiveWindowBorderStyle;
|
||||
use komorebi_core::Rect;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
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::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::ValidateRect;
|
||||
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 message = MSG::default();
|
||||
unsafe {
|
||||
while GetMessageW(&mut message, HWND(hwnd), 0, 0).into() {
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
}
|
||||
|
||||
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) -> 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));
|
||||
|
||||
// Store the border rect so that it can be used by the callback
|
||||
{
|
||||
let mut rects = RECT_STATE.lock();
|
||||
rects.insert(self.hwnd, rect);
|
||||
}
|
||||
|
||||
// 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.lock()).into()))?;
|
||||
}
|
||||
|
||||
// Invalidate the rect to trigger the callback to update colours etc.
|
||||
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 rects = RECT_STATE.lock();
|
||||
|
||||
// With the rect that we stored in Self::update
|
||||
if let Some(rect) = rects.get(&window.0).copied() {
|
||||
// Grab the focus kind for this border
|
||||
let focus_kind = {
|
||||
FOCUS_STATE
|
||||
.lock()
|
||||
.get(&window.0)
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused)
|
||||
};
|
||||
|
||||
// Set up the brush to draw the border
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
let hpen = CreatePen(
|
||||
PS_SOLID | PS_INSIDEFRAME,
|
||||
BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
COLORREF(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),
|
||||
}),
|
||||
);
|
||||
|
||||
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.lock() {
|
||||
ActiveWindowBorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
} else {
|
||||
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
ActiveWindowBorderStyle::Rounded => {
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
}
|
||||
ActiveWindowBorderStyle::Square => {
|
||||
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
EndPaint(window, &ps);
|
||||
ValidateRect(window, None);
|
||||
}
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(window, message, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
362
komorebi/src/border_manager/mod.rs
Normal file
362
komorebi/src/border_manager/mod.rs
Normal file
@@ -0,0 +1,362 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod border;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::ActiveWindowBorderStyle;
|
||||
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::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::Colour;
|
||||
use crate::Rect;
|
||||
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);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref Z_ORDER: Arc<Mutex<ZOrder>> = Arc::new(Mutex::new(ZOrder::Bottom));
|
||||
pub static ref STYLE: Arc<Mutex<ActiveWindowBorderStyle>> =
|
||||
Arc::new(Mutex::new(ActiveWindowBorderStyle::System));
|
||||
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 RECT_STATE: Mutex<HashMap<isize, Rect>> = 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::unbounded)
|
||||
}
|
||||
|
||||
pub fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
pub fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
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();
|
||||
RECT_STATE.lock().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(())
|
||||
}
|
||||
|
||||
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();
|
||||
let mut instant: Option<Instant> = None;
|
||||
event_tx().send(Notification)?;
|
||||
|
||||
'receiver: for _ in receiver {
|
||||
if let Some(instant) = instant {
|
||||
if instant.elapsed().lt(&Duration::from_millis(50)) {
|
||||
continue 'receiver;
|
||||
}
|
||||
}
|
||||
|
||||
instant = Some(Instant::now());
|
||||
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
let mut borders_monitors = BORDERS_MONITORS.lock();
|
||||
|
||||
// Check the wm state every time we receive a notification
|
||||
let state = wm.lock();
|
||||
|
||||
// If borders are disabled
|
||||
if !BORDER_ENABLED.load_consume()
|
||||
// Or if the wm is paused
|
||||
|| state.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();
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
let focused_monitor_idx = state.focused_monitor_idx();
|
||||
|
||||
for (monitor_idx, m) in state.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 'receiver;
|
||||
}
|
||||
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
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);
|
||||
}
|
||||
|
||||
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 'receiver;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(border.hwnd, WindowKind::Monocle);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
|
||||
border.update(&rect)?;
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
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 'receiver;
|
||||
}
|
||||
|
||||
// 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 state.pending_move_op.is_some() && idx == ws.focused_container_idx() {
|
||||
let restore_z_order = *Z_ORDER.lock();
|
||||
*Z_ORDER.lock() = 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 'receiver;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
|
||||
if rect != new_rect {
|
||||
rect = new_rect;
|
||||
border.update(&rect)?;
|
||||
}
|
||||
}
|
||||
|
||||
*Z_ORDER.lock() = restore_z_order;
|
||||
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
// 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 'receiver;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(c.id().clone(), monitor_idx);
|
||||
|
||||
// Update the focused state for all containers on this workspace
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(
|
||||
border.hwnd,
|
||||
if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
} else if c.windows().len() > 1 {
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
WindowKind::Single
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
|
||||
border.update(&rect)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
103
komorebi/src/colour.rs
Normal file
103
komorebi/src/colour.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use hex_color::HexColor;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::InstanceType;
|
||||
use schemars::schema::Schema;
|
||||
use schemars::schema::SchemaObject;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum Colour {
|
||||
/// Colour represented as RGB
|
||||
Rgb(Rgb),
|
||||
/// Colour represented as Hex
|
||||
Hex(Hex),
|
||||
}
|
||||
|
||||
impl From<Rgb> for Colour {
|
||||
fn from(value: Rgb) -> Self {
|
||||
Self::Rgb(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Colour {
|
||||
fn from(value: u32) -> Self {
|
||||
Self::Rgb(Rgb::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct Hex(HexColor);
|
||||
|
||||
impl JsonSchema for Hex {
|
||||
fn schema_name() -> String {
|
||||
String::from("Hex")
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Colour> for u32 {
|
||||
fn from(value: Colour) -> Self {
|
||||
match value {
|
||||
Colour::Rgb(val) => val.into(),
|
||||
Colour::Hex(val) => (Rgb::from(val)).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Rgb {
|
||||
/// Red
|
||||
pub r: u32,
|
||||
/// Green
|
||||
pub g: u32,
|
||||
/// Blue
|
||||
pub b: u32,
|
||||
}
|
||||
|
||||
impl Rgb {
|
||||
pub const fn new(r: u32, g: u32, b: u32) -> Self {
|
||||
Self { r, g, b }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hex> for Rgb {
|
||||
fn from(value: Hex) -> Self {
|
||||
value.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HexColor> for Rgb {
|
||||
fn from(value: HexColor) -> Self {
|
||||
Self {
|
||||
r: value.r as u32,
|
||||
g: value.g as u32,
|
||||
b: value.b as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rgb> for u32 {
|
||||
fn from(value: Rgb) -> Self {
|
||||
value.r | (value.g << 8) | (value.b << 16)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Rgb {
|
||||
fn from(value: u32) -> Self {
|
||||
Self {
|
||||
r: value & 0xff,
|
||||
g: value >> 8 & 0xff,
|
||||
b: value >> 16 & 0xff,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ use interfaces::IServiceProvider;
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use windows::core::ComInterface;
|
||||
use windows::core::Interface;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::System::Com::CoCreateInstance;
|
||||
|
||||
@@ -3,17 +3,24 @@ use std::collections::VecDeque;
|
||||
use getset::Getters;
|
||||
use nanoid::nanoid;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::ring::Ring;
|
||||
use crate::stackbar::Stackbar;
|
||||
use crate::window::Window;
|
||||
use crate::WindowsApi;
|
||||
use crate::STACKBAR_MODE;
|
||||
use komorebi_core::StackbarMode;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Getters, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
|
||||
pub struct Container {
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
windows: Ring<Window>,
|
||||
#[serde(skip)]
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
stackbar: Option<Stackbar>,
|
||||
}
|
||||
|
||||
impl_ring_elements!(Container, Window);
|
||||
@@ -23,6 +30,10 @@ 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +45,38 @@ 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();
|
||||
|
||||
if !should_hide {
|
||||
if let Some(omit) = omit {
|
||||
if omit != window.hwnd {
|
||||
should_hide = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_hide {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore(&self) {
|
||||
if let Some(stackbar) = self.stackbar() {
|
||||
stackbar.restore();
|
||||
}
|
||||
|
||||
if let Some(window) = self.focused_window() {
|
||||
window.restore();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_focused_window(&mut self) {
|
||||
let focused_idx = self.focused_window_idx();
|
||||
for (i, window) in self.windows_mut().iter_mut().enumerate() {
|
||||
@@ -81,6 +124,13 @@ 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 {
|
||||
if let Some(stackbar) = &self.stackbar {
|
||||
let _ = WindowsApi::close_window(stackbar.hwnd());
|
||||
self.stackbar = None;
|
||||
}
|
||||
}
|
||||
|
||||
if idx != 0 {
|
||||
self.focus_window(idx - 1);
|
||||
};
|
||||
@@ -95,6 +145,14 @@ impl Container {
|
||||
|
||||
pub fn add_window(&mut self, window: Window) {
|
||||
self.windows_mut().push_back(window);
|
||||
|
||||
if matches!(*STACKBAR_MODE.lock(), StackbarMode::OnStack)
|
||||
&& self.windows().len() > 1
|
||||
&& self.stackbar.is_none()
|
||||
{
|
||||
self.stackbar = Stackbar::create().ok();
|
||||
}
|
||||
|
||||
self.focus_window(self.windows().len() - 1);
|
||||
}
|
||||
|
||||
@@ -103,4 +161,41 @@ impl Container {
|
||||
tracing::info!("focusing window");
|
||||
self.windows.focus(idx);
|
||||
}
|
||||
|
||||
pub fn set_stackbar_mode(&mut self, mode: StackbarMode) {
|
||||
match mode {
|
||||
StackbarMode::Always => {
|
||||
if self.stackbar.is_none() {
|
||||
self.stackbar = Stackbar::create().ok();
|
||||
}
|
||||
}
|
||||
StackbarMode::Never => {
|
||||
if let Some(stackbar) = &self.stackbar {
|
||||
let _ = WindowsApi::close_window(stackbar.hwnd());
|
||||
}
|
||||
|
||||
self.stackbar = None
|
||||
}
|
||||
StackbarMode::OnStack => {
|
||||
if self.windows().len() > 1 && self.stackbar().is_none() {
|
||||
self.stackbar = Stackbar::create().ok();
|
||||
}
|
||||
|
||||
if let Some(stackbar) = &self.stackbar {
|
||||
if self.windows().len() == 1 {
|
||||
let _ = WindowsApi::close_window(stackbar.hwnd());
|
||||
self.stackbar = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn renew_stackbar(&mut self) {
|
||||
if let Some(stackbar) = &self.stackbar {
|
||||
if !WindowsApi::is_window(stackbar.hwnd()) {
|
||||
self.stackbar = Stackbar::create().ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use windows::core::PCWSTR;
|
||||
@@ -15,7 +14,6 @@ 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 {
|
||||
@@ -37,7 +35,7 @@ impl Hidden {
|
||||
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 brush = WindowsApi::create_solid_brush(0);
|
||||
let window_class = WNDCLASSW {
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: class_name,
|
||||
@@ -59,7 +57,6 @@ impl Hidden {
|
||||
unsafe {
|
||||
while GetMessageW(&mut message, hidden.hwnd(), 0, 0).into() {
|
||||
DispatchMessageW(&message);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
362
komorebi/src/lib.rs
Normal file
362
komorebi/src/lib.rs
Normal file
@@ -0,0 +1,362 @@
|
||||
pub mod border_manager;
|
||||
pub mod com;
|
||||
#[macro_use]
|
||||
pub mod ring;
|
||||
pub mod colour;
|
||||
pub mod container;
|
||||
pub mod hidden;
|
||||
pub mod monitor;
|
||||
pub mod process_command;
|
||||
pub mod process_event;
|
||||
pub mod process_movement;
|
||||
pub mod set_window_position;
|
||||
pub mod stackbar;
|
||||
pub mod static_config;
|
||||
pub mod styles;
|
||||
pub mod window;
|
||||
pub mod window_manager;
|
||||
pub mod window_manager_event;
|
||||
pub mod windows_api;
|
||||
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;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
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::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use colour::*;
|
||||
pub use hidden::*;
|
||||
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;
|
||||
pub use windows_api::*;
|
||||
|
||||
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::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
use komorebi_core::StackbarMode;
|
||||
use os_info::Version;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixStream;
|
||||
use which::which;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
use winreg::RegKey;
|
||||
|
||||
type WorkspaceRule = (usize, usize, bool);
|
||||
|
||||
lazy_static! {
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("steam.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
]));
|
||||
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> =
|
||||
Arc::new(Mutex::new(vec![
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("explorer.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("firefox.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("chrome.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("idea64.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("ApplicationFrameHost.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("steam.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
]));
|
||||
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("firefox.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("idea64.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
]));
|
||||
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref DISPLAY_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, String>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, WorkspaceRule>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
// mstsc.exe creates these on Windows 11 when a WSL process is launched
|
||||
// https://github.com/LGUG2Z/komorebi/issues/74
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: String::from("OPContainerClass"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: String::from("IHWindowClass"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
]));
|
||||
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"X410.exe".to_string(),
|
||||
"vcxsrv.exe".to_string(),
|
||||
]));
|
||||
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
|
||||
Arc::new(Mutex::new(HidingBehaviour::Minimize));
|
||||
pub static ref HOME_DIR: PathBuf = {
|
||||
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!(
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
|
||||
);
|
||||
}
|
||||
})
|
||||
};
|
||||
pub static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
|
||||
pub static ref AHK_EXE: String = {
|
||||
let mut ahk: String = String::from("autohotkey.exe");
|
||||
|
||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
|
||||
if which(&komorebi_ahk_exe).is_ok() {
|
||||
ahk = komorebi_ahk_exe;
|
||||
}
|
||||
}
|
||||
|
||||
ahk
|
||||
};
|
||||
static ref WINDOWS_11: bool = {
|
||||
matches!(
|
||||
os_info::get().version(),
|
||||
Version::Semantic(_, _, x) if x >= &22000
|
||||
)
|
||||
};
|
||||
|
||||
// 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()));
|
||||
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
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 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);
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
|
||||
// This is the path on Windows 10
|
||||
let mut current = hkcu
|
||||
.open_subkey(format!(
|
||||
r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#,
|
||||
SESSION_ID.load(Ordering::SeqCst)
|
||||
))
|
||||
.ok()
|
||||
.and_then(
|
||||
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||
Ok(current) => Option::from(current.bytes),
|
||||
Err(_) => None,
|
||||
},
|
||||
);
|
||||
|
||||
// This is the path on Windows 11
|
||||
if current.is_none() {
|
||||
current = hkcu
|
||||
.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops")
|
||||
.ok()
|
||||
.and_then(
|
||||
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||
Ok(current) => Option::from(current.bytes),
|
||||
Err(_) => None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not
|
||||
// exist until one has been created in the task view
|
||||
|
||||
// The registry value will also not exist on user login if virtual desktops have been created
|
||||
// but the task view has not been initiated
|
||||
|
||||
// In both of these cases, we return None, and the virtual desktop validation will never run. In
|
||||
// the latter case, if the user desires this validation after initiating the task view, komorebi
|
||||
// should be restarted, and then when this // fn runs again for the first time, it will pick up
|
||||
// the value of CurrentVirtualDesktop and validate against it accordingly
|
||||
current
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
let mut stale_sockets = vec![];
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
|
||||
for (socket, path) in &mut *sockets {
|
||||
match UnixStream::connect(path) {
|
||||
Ok(mut stream) => {
|
||||
tracing::debug!("pushed notification to subscriber: {socket}");
|
||||
stream.write_all(notification.as_bytes())?;
|
||||
}
|
||||
Err(_) => {
|
||||
stale_sockets.push(socket.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for socket in stale_sockets {
|
||||
tracing::warn!("removing stale subscription: {socket}");
|
||||
sockets.remove(&socket);
|
||||
}
|
||||
|
||||
let mut stale_pipes = vec![];
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
for (subscriber, pipe) in &mut *pipes {
|
||||
match writeln!(pipe, "{notification}") {
|
||||
Ok(()) => {
|
||||
tracing::debug!("pushed notification to subscriber: {subscriber}");
|
||||
}
|
||||
Err(error) => {
|
||||
// ERROR_FILE_NOT_FOUND
|
||||
// 2 (0x2)
|
||||
// The system cannot find the file specified.
|
||||
|
||||
// ERROR_NO_DATA
|
||||
// 232 (0xE8)
|
||||
// The pipe is being closed.
|
||||
|
||||
// Remove the subscription; the process will have to subscribe again
|
||||
if let Some(2 | 232) = error.raw_os_error() {
|
||||
stale_pipes.push(subscriber.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for subscriber in stale_pipes {
|
||||
tracing::warn!("removing stale subscription: {}", subscriber);
|
||||
pipes.remove(&subscriber);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_configuration() -> Result<()> {
|
||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
|
||||
if config_pwsh.exists() {
|
||||
let powershell_exe = if which("pwsh.exe").is_ok() {
|
||||
"pwsh.exe"
|
||||
} else {
|
||||
"powershell.exe"
|
||||
};
|
||||
|
||||
tracing::info!("loading configuration file: {}", config_pwsh.display());
|
||||
|
||||
Command::new(powershell_exe)
|
||||
.arg(config_pwsh.as_os_str())
|
||||
.output()?;
|
||||
} else if config_ahk.exists() && which(&*AHK_EXE).is_ok() {
|
||||
tracing::info!("loading configuration file: {}", config_ahk.display());
|
||||
|
||||
Command::new(&*AHK_EXE)
|
||||
.arg(config_ahk.as_os_str())
|
||||
.output()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,16 +6,7 @@
|
||||
clippy::significant_drop_in_scrutinee
|
||||
)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
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::Ordering;
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
@@ -23,222 +14,32 @@ use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::Backoff;
|
||||
use lazy_static::lazy_static;
|
||||
use os_info::Version;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use sysinfo::Process;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use which::which;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
use winreg::RegKey;
|
||||
|
||||
use crate::hidden::Hidden;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
|
||||
use crate::process_command::listen_for_commands;
|
||||
use crate::process_command::listen_for_commands_tcp;
|
||||
use crate::process_event::listen_for_events;
|
||||
use crate::process_movement::listen_for_movements;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::window_manager::State;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
|
||||
#[macro_use]
|
||||
mod ring;
|
||||
|
||||
mod border;
|
||||
mod com;
|
||||
mod container;
|
||||
mod hidden;
|
||||
mod monitor;
|
||||
mod process_command;
|
||||
mod process_event;
|
||||
mod process_movement;
|
||||
mod set_window_position;
|
||||
mod static_config;
|
||||
mod styles;
|
||||
mod window;
|
||||
mod window_manager;
|
||||
mod window_manager_event;
|
||||
mod windows_api;
|
||||
mod windows_callbacks;
|
||||
mod winevent;
|
||||
mod winevent_listener;
|
||||
mod workspace;
|
||||
|
||||
type WorkspaceRule = (usize, usize, bool);
|
||||
|
||||
lazy_static! {
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("steam.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
]));
|
||||
static ref TRAY_AND_MULTI_WINDOW_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> =
|
||||
Arc::new(Mutex::new(vec![
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("explorer.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("firefox.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("chrome.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("idea64.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("ApplicationFrameHost.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("steam.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}
|
||||
]));
|
||||
static ref OBJECT_NAME_CHANGE_ON_LAUNCH: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("firefox.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("idea64.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
]));
|
||||
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref DISPLAY_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, String>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, WorkspaceRule>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![
|
||||
// mstsc.exe creates these on Windows 11 when a WSL process is launched
|
||||
// https://github.com/LGUG2Z/komorebi/issues/74
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: String::from("OPContainerClass"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
},
|
||||
IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: String::from("IHWindowClass"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}
|
||||
]));
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
static ref BORDER_OVERFLOW_IDENTIFIERS: Arc<Mutex<Vec<IdWithIdentifier>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref WSL2_UI_PROCESSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"X410.exe".to_string(),
|
||||
"vcxsrv.exe".to_string(),
|
||||
]));
|
||||
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
|
||||
Arc::new(Mutex::new(HidingBehaviour::Minimize));
|
||||
static ref HOME_DIR: PathBuf = {
|
||||
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!(
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
|
||||
);
|
||||
}
|
||||
})
|
||||
};
|
||||
static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
|
||||
static ref AHK_EXE: String = {
|
||||
let mut ahk: String = String::from("autohotkey.exe");
|
||||
|
||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
|
||||
if which(&komorebi_ahk_exe).is_ok() {
|
||||
ahk = komorebi_ahk_exe;
|
||||
}
|
||||
}
|
||||
|
||||
ahk
|
||||
};
|
||||
static ref WINDOWS_11: bool = {
|
||||
matches!(
|
||||
os_info::get().version(),
|
||||
Version::Semantic(_, _, x) if x >= &22000
|
||||
)
|
||||
};
|
||||
|
||||
static ref BORDER_RECT: Arc<Mutex<Rect>> =
|
||||
Arc::new(Mutex::new(Rect::default()));
|
||||
|
||||
static ref BORDER_OFFSET: Arc<Mutex<Option<Rect>>> =
|
||||
Arc::new(Mutex::new(None));
|
||||
|
||||
// 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![]));
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
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 ALT_FOCUS_HACK: AtomicBool = AtomicBool::new(false);
|
||||
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(20);
|
||||
// 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);
|
||||
use komorebi::border_manager;
|
||||
use komorebi::hidden::Hidden;
|
||||
use komorebi::load_configuration;
|
||||
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::static_config::StaticConfig;
|
||||
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;
|
||||
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
@@ -303,124 +104,6 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
Ok((guard, color_guard))
|
||||
}
|
||||
|
||||
pub fn load_configuration() -> Result<()> {
|
||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
|
||||
if config_pwsh.exists() {
|
||||
let powershell_exe = if which("pwsh.exe").is_ok() {
|
||||
"pwsh.exe"
|
||||
} else {
|
||||
"powershell.exe"
|
||||
};
|
||||
|
||||
tracing::info!("loading configuration file: {}", config_pwsh.display());
|
||||
|
||||
Command::new(powershell_exe)
|
||||
.arg(config_pwsh.as_os_str())
|
||||
.output()?;
|
||||
} else if config_ahk.exists() && which(&*AHK_EXE).is_ok() {
|
||||
tracing::info!("loading configuration file: {}", config_ahk.display());
|
||||
|
||||
Command::new(&*AHK_EXE)
|
||||
.arg(config_ahk.as_os_str())
|
||||
.output()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
|
||||
// This is the path on Windows 10
|
||||
let mut current = hkcu
|
||||
.open_subkey(format!(
|
||||
r#"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SessionInfo\{}\VirtualDesktops"#,
|
||||
SESSION_ID.load(Ordering::SeqCst)
|
||||
))
|
||||
.ok()
|
||||
.and_then(
|
||||
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||
Ok(current) => Option::from(current.bytes),
|
||||
Err(_) => None,
|
||||
},
|
||||
);
|
||||
|
||||
// This is the path on Windows 11
|
||||
if current.is_none() {
|
||||
current = hkcu
|
||||
.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VirtualDesktops")
|
||||
.ok()
|
||||
.and_then(
|
||||
|desktops| match desktops.get_raw_value("CurrentVirtualDesktop") {
|
||||
Ok(current) => Option::from(current.bytes),
|
||||
Err(_) => None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// For Win10 users that do not use virtual desktops, the CurrentVirtualDesktop value will not
|
||||
// exist until one has been created in the task view
|
||||
|
||||
// The registry value will also not exist on user login if virtual desktops have been created
|
||||
// but the task view has not been initiated
|
||||
|
||||
// In both of these cases, we return None, and the virtual desktop validation will never run. In
|
||||
// the latter case, if the user desires this validation after initiating the task view, komorebi
|
||||
// should be restarted, and then when this // fn runs again for the first time, it will pick up
|
||||
// the value of CurrentVirtualDesktop and validate against it accordingly
|
||||
current
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
let mut stale_subscriptions = vec![];
|
||||
let mut subscriptions = SUBSCRIPTION_PIPES.lock();
|
||||
for (subscriber, pipe) in &mut *subscriptions {
|
||||
match writeln!(pipe, "{notification}") {
|
||||
Ok(()) => {
|
||||
tracing::debug!("pushed notification to subscriber: {}", subscriber);
|
||||
}
|
||||
Err(error) => {
|
||||
// ERROR_FILE_NOT_FOUND
|
||||
// 2 (0x2)
|
||||
// The system cannot find the file specified.
|
||||
|
||||
// ERROR_NO_DATA
|
||||
// 232 (0xE8)
|
||||
// The pipe is being closed.
|
||||
|
||||
// Remove the subscription; the process will have to subscribe again
|
||||
if let Some(2 | 232) = error.raw_os_error() {
|
||||
let subscriber_cl = subscriber.clone();
|
||||
stale_subscriptions.push(subscriber_cl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for subscriber in stale_subscriptions {
|
||||
tracing::warn!("removing stale subscription: {}", subscriber);
|
||||
subscriptions.remove(&subscriber);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
#[tracing::instrument]
|
||||
fn detect_deadlocks() {
|
||||
@@ -500,15 +183,11 @@ fn main() -> Result<()> {
|
||||
|
||||
WindowsApi::foreground_lock_timeout()?;
|
||||
|
||||
winevent_listener::start();
|
||||
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
detect_deadlocks();
|
||||
|
||||
let (outgoing, incoming): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
|
||||
crossbeam_channel::unbounded();
|
||||
|
||||
let winevent_listener = winevent_listener::new(Arc::new(Mutex::new(outgoing)));
|
||||
winevent_listener.start();
|
||||
|
||||
Hidden::create("komorebi-hidden")?;
|
||||
|
||||
let static_config = opts.config.map_or_else(
|
||||
@@ -523,6 +202,8 @@ fn main() -> Result<()> {
|
||||
Option::from,
|
||||
);
|
||||
|
||||
std::fs::create_dir_all(&*DATA_DIR)?;
|
||||
|
||||
let wm = if let Some(config) = &static_config {
|
||||
tracing::info!(
|
||||
"creating window manager from static configuration file: {}",
|
||||
@@ -531,12 +212,12 @@ fn main() -> Result<()> {
|
||||
|
||||
Arc::new(Mutex::new(StaticConfig::preload(
|
||||
config,
|
||||
Arc::new(Mutex::new(incoming)),
|
||||
winevent_listener::event_rx(),
|
||||
)?))
|
||||
} else {
|
||||
Arc::new(Mutex::new(WindowManager::new(Arc::new(Mutex::new(
|
||||
incoming,
|
||||
)))?))
|
||||
Arc::new(Mutex::new(WindowManager::new(
|
||||
winevent_listener::event_rx(),
|
||||
)?))
|
||||
};
|
||||
|
||||
wm.lock().init()?;
|
||||
@@ -574,6 +255,9 @@ fn main() -> Result<()> {
|
||||
listen_for_movements(wm.clone());
|
||||
}
|
||||
|
||||
border_manager::listen_for_notifications(wm.clone());
|
||||
workspace_reconciliator::listen_for_notifications(wm.clone());
|
||||
|
||||
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
||||
ctrlc::set_handler(move || {
|
||||
ctrlc_sender
|
||||
|
||||
@@ -9,6 +9,7 @@ use getset::Getters;
|
||||
use getset::MutGetters;
|
||||
use getset::Setters;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::Rect;
|
||||
@@ -17,7 +18,9 @@ use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
|
||||
#[derive(
|
||||
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
|
||||
)]
|
||||
pub struct Monitor {
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
id: isize,
|
||||
@@ -33,11 +36,14 @@ pub struct Monitor {
|
||||
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)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
last_focused_workspace: Option<usize>,
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get_mut = "pub")]
|
||||
workspace_names: HashMap<usize, String>,
|
||||
}
|
||||
@@ -56,6 +62,8 @@ pub fn new(id: isize, size: Rect, work_area_size: Rect, name: String) -> Monitor
|
||||
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(),
|
||||
@@ -69,7 +77,7 @@ impl Monitor {
|
||||
if i == focused_idx {
|
||||
workspace.restore(mouse_follows_focus)?;
|
||||
} else {
|
||||
workspace.hide();
|
||||
workspace.hide(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,12 +198,13 @@ impl Monitor {
|
||||
self.workspaces().len()
|
||||
}
|
||||
|
||||
pub fn update_focused_workspace(
|
||||
&mut self,
|
||||
offset: Option<Rect>,
|
||||
invisible_borders: &Rect,
|
||||
) -> Result<()> {
|
||||
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 {
|
||||
@@ -204,7 +213,7 @@ impl Monitor {
|
||||
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
.update(&work_area, offset, invisible_borders)?;
|
||||
.update(&work_area, offset, window_based_work_area_offset)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::fs::OpenOptions;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::TcpListener;
|
||||
use std::net::TcpStream;
|
||||
use std::num::NonZeroUsize;
|
||||
@@ -24,6 +23,7 @@ use uds_windows::UnixStream;
|
||||
|
||||
use komorebi_core::config_generation::ApplicationConfiguration;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::Axis;
|
||||
@@ -38,27 +38,20 @@ use komorebi_core::StateQuery;
|
||||
use komorebi_core::WindowContainerBehaviour;
|
||||
use komorebi_core::WindowKind;
|
||||
|
||||
use crate::border::Border;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::colour::Rgb;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::notify_subscribers;
|
||||
use crate::static_config::StaticConfig;
|
||||
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::ALT_FOCUS_HACK;
|
||||
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_OVERFLOW_IDENTIFIERS;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
@@ -71,30 +64,48 @@ use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
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::SUBSCRIPTION_PIPES;
|
||||
use crate::SUBSCRIPTION_SOCKETS;
|
||||
use crate::TCP_CONNECTIONS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
|
||||
#[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 listener = wm
|
||||
.lock()
|
||||
.command_listener
|
||||
.try_clone()
|
||||
.expect("could not clone unix listener");
|
||||
|
||||
std::thread::spawn(move || {
|
||||
tracing::info!("listening on komorebi.sock");
|
||||
|
||||
// client is unique for every komorebic command
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(stream) => match read_commands_uds(&wm, stream) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
Err(error) => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!("{:?}", error)
|
||||
} else {
|
||||
tracing::error!("{}", error)
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error);
|
||||
break;
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!("{:?}", error)
|
||||
} else {
|
||||
tracing::error!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,8 +155,15 @@ pub fn listen_for_commands_tcp(wm: Arc<Mutex<WindowManager>>, port: usize) {
|
||||
}
|
||||
|
||||
impl WindowManager {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn process_command(&mut self, message: SocketMessage) -> Result<()> {
|
||||
// TODO(raggi): wrap reply in a newtype that can decorate a human friendly
|
||||
// name for the peer, such as getting the pid of the komorebic process for
|
||||
// the UDS or the IP:port for TCP.
|
||||
#[tracing::instrument(skip(self, reply))]
|
||||
pub fn process_command(
|
||||
&mut self,
|
||||
message: SocketMessage,
|
||||
mut reply: impl std::io::Write,
|
||||
) -> Result<()> {
|
||||
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
|
||||
if let Some(id) = current_virtual_desktop() {
|
||||
if id != *virtual_desktop_id {
|
||||
@@ -158,21 +176,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() {
|
||||
@@ -209,6 +212,7 @@ impl WindowManager {
|
||||
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
|
||||
SocketMessage::CycleStack(direction) => {
|
||||
self.cycle_container_window_in_direction(direction)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
SocketMessage::ForceFocus => {
|
||||
let focused_window = self.focused_window()?;
|
||||
@@ -266,17 +270,19 @@ impl WindowManager {
|
||||
|
||||
let mut should_push = true;
|
||||
for m in &*manage_identifiers {
|
||||
if m.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(m) = m {
|
||||
if m.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
manage_identifiers.push(IdWithIdentifier {
|
||||
manage_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::FloatRule(identifier, ref id) => {
|
||||
@@ -284,20 +290,21 @@ impl WindowManager {
|
||||
|
||||
let mut should_push = true;
|
||||
for f in &*float_identifiers {
|
||||
if f.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(f) = f {
|
||||
if f.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
float_identifiers.push(IdWithIdentifier {
|
||||
float_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
let mut hwnds_to_purge = vec![];
|
||||
@@ -309,6 +316,11 @@ impl WindowManager {
|
||||
{
|
||||
for window in container.windows() {
|
||||
match identifier {
|
||||
ApplicationIdentifier::Path => {
|
||||
if window.path()? == *id {
|
||||
hwnds_to_purge.push((i, window.hwnd));
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if window.exe()? == *id {
|
||||
hwnds_to_purge.push((i, window.hwnd));
|
||||
@@ -340,7 +352,7 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no focused workspace"))?
|
||||
.remove_window(hwnd)?;
|
||||
|
||||
monitor.update_focused_workspace(offset, &invisible_borders)?;
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusedWorkspaceContainerPadding(adjustment) => {
|
||||
@@ -439,6 +451,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)
|
||||
@@ -482,13 +497,16 @@ 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::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)?,
|
||||
@@ -611,10 +629,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
|
||||
@@ -638,10 +652,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
|
||||
@@ -651,11 +661,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
|
||||
@@ -675,14 +683,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)) =
|
||||
@@ -691,10 +702,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!(
|
||||
@@ -743,15 +750,23 @@ impl WindowManager {
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
tracing::info!("replying to state");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(state.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(state.as_bytes())?;
|
||||
|
||||
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();
|
||||
@@ -774,15 +789,7 @@ impl WindowManager {
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(visible_windows_state.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(visible_windows_state.as_bytes())?;
|
||||
}
|
||||
|
||||
SocketMessage::Query(query) => {
|
||||
@@ -801,15 +808,7 @@ impl WindowManager {
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(response.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(response.as_bytes())?;
|
||||
}
|
||||
SocketMessage::ResizeWindowEdge(direction, sizing) => {
|
||||
self.resize_window(direction, sizing, self.resize_delta, true)?;
|
||||
@@ -917,7 +916,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) {
|
||||
@@ -1023,63 +1022,49 @@ 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) => {
|
||||
self.watch_configuration(enable)?;
|
||||
}
|
||||
SocketMessage::IdentifyBorderOverflowApplication(identifier, ref id) => {
|
||||
let mut identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}
|
||||
}
|
||||
SocketMessage::IdentifyObjectNameChangeApplication(identifier, ref id) => {
|
||||
let mut identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(i) = i {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::IdentifyTrayApplication(identifier, ref id) => {
|
||||
let mut identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(i) = i {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::IdentifyLayeredApplication(identifier, ref id) => {
|
||||
@@ -1087,17 +1072,19 @@ impl WindowManager {
|
||||
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
if let MatchingRule::Simple(i) = i {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(IdWithIdentifier {
|
||||
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::ManageFocusedWindow => {
|
||||
@@ -1106,10 +1093,7 @@ impl WindowManager {
|
||||
SocketMessage::UnmanageFocusedWindow => {
|
||||
self.unmanage_focused_window()?;
|
||||
}
|
||||
SocketMessage::InvisibleBorders(rect) => {
|
||||
self.invisible_borders = rect;
|
||||
self.retile_all(false)?;
|
||||
}
|
||||
SocketMessage::InvisibleBorders(_rect) => {}
|
||||
SocketMessage::WorkAreaOffset(rect) => {
|
||||
self.work_area_offset = Option::from(rect);
|
||||
self.retile_all(false)?;
|
||||
@@ -1145,7 +1129,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()?;
|
||||
@@ -1168,9 +1152,18 @@ 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::AddSubscriber(ref subscriber) => {
|
||||
SocketMessage::AddSubscriberSocket(ref socket) => {
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
let socket_path = DATA_DIR.join(socket);
|
||||
sockets.insert(socket.clone(), socket_path);
|
||||
}
|
||||
SocketMessage::RemoveSubscriberSocket(ref socket) => {
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
sockets.remove(socket);
|
||||
}
|
||||
SocketMessage::AddSubscriberPipe(ref subscriber) => {
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
let pipe_path = format!(r"\\.\pipe\{subscriber}");
|
||||
let pipe = connect(&pipe_path).map_err(|_| {
|
||||
@@ -1179,7 +1172,7 @@ impl WindowManager {
|
||||
|
||||
pipes.insert(subscriber.clone(), pipe);
|
||||
}
|
||||
SocketMessage::RemoveSubscriber(ref subscriber) => {
|
||||
SocketMessage::RemoveSubscriberPipe(ref subscriber) => {
|
||||
let mut pipes = SUBSCRIPTION_PIPES.lock();
|
||||
pipes.remove(subscriber);
|
||||
}
|
||||
@@ -1214,6 +1207,7 @@ impl WindowManager {
|
||||
MoveBehaviour::Insert => {
|
||||
self.cross_monitor_move_behaviour = MoveBehaviour::Swap;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
SocketMessage::CrossMonitorMoveBehaviour(behaviour) => {
|
||||
@@ -1223,93 +1217,79 @@ impl WindowManager {
|
||||
self.unmanaged_window_operation_behaviour = behaviour;
|
||||
}
|
||||
SocketMessage::ActiveWindowBorder(enable) => {
|
||||
if enable {
|
||||
if BORDER_HWND.load(Ordering::SeqCst) == 0 {
|
||||
Border::create("komorebi-border-window")?;
|
||||
}
|
||||
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::ActiveWindowBorderColour(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::ActiveWindowBorderStyle(style) => {
|
||||
let mut active_window_border_style = STYLE.lock();
|
||||
*active_window_border_style = style;
|
||||
}
|
||||
SocketMessage::BorderWidth(width) => {
|
||||
border_manager::BORDER_WIDTH.store(width, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::BorderOffset(offset) => {
|
||||
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::StackbarMode(mode) => {
|
||||
let mut stackbar_mode = STACKBAR_MODE.lock();
|
||||
*stackbar_mode = mode;
|
||||
|
||||
BORDER_ENABLED.store(true, Ordering::SeqCst);
|
||||
self.show_border()?;
|
||||
} else {
|
||||
BORDER_ENABLED.store(false, Ordering::SeqCst);
|
||||
self.hide_border()?;
|
||||
for m in self.monitors_mut() {
|
||||
for w in m.workspaces_mut() {
|
||||
for c in w.containers_mut() {
|
||||
c.set_stackbar_mode(mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::ActiveWindowBorderColour(kind, r, g, b) => {
|
||||
match kind {
|
||||
WindowKind::Single => {
|
||||
BORDER_COLOUR_SINGLE.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
|
||||
BORDER_COLOUR_CURRENT.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Stack => {
|
||||
BORDER_COLOUR_STACK.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Monocle => {
|
||||
BORDER_COLOUR_MONOCLE.store(r | (g << 8) | (b << 16), Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
SocketMessage::StackbarFocusedTextColour(r, g, b) => {
|
||||
let rgb = Rgb::new(r, g, b);
|
||||
STACKBAR_FOCUSED_TEXT_COLOUR.store(rgb.into(), Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::ActiveWindowBorderWidth(width) => {
|
||||
BORDER_WIDTH.store(width, Ordering::SeqCst);
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
SocketMessage::StackbarUnfocusedTextColour(r, g, b) => {
|
||||
let rgb = Rgb::new(r, g, b);
|
||||
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(rgb.into(), Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::ActiveWindowBorderOffset(offset) => {
|
||||
let mut current_border_offset = BORDER_OFFSET.lock();
|
||||
|
||||
let new_border_offset = Rect {
|
||||
left: offset,
|
||||
top: offset,
|
||||
right: offset * 2,
|
||||
bottom: offset * 2,
|
||||
};
|
||||
|
||||
*current_border_offset = Option::from(new_border_offset);
|
||||
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
SocketMessage::StackbarBackgroundColour(r, g, b) => {
|
||||
let rgb = Rgb::new(r, g, b);
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.store(rgb.into(), Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::AltFocusHack(enable) => {
|
||||
ALT_FOCUS_HACK.store(enable, Ordering::SeqCst);
|
||||
SocketMessage::StackbarHeight(height) => {
|
||||
STACKBAR_TAB_HEIGHT.store(height, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::StackbarTabWidth(width) => {
|
||||
STACKBAR_TAB_WIDTH.store(width, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::ApplicationSpecificConfigurationSchema => {
|
||||
let asc = schema_for!(Vec<ApplicationConfiguration>);
|
||||
let schema = serde_json::to_string_pretty(&asc)?;
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::NotificationSchema => {
|
||||
let notification = schema_for!(Notification);
|
||||
let schema = serde_json::to_string_pretty(¬ification)?;
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::SocketSchema => {
|
||||
let socket_message = schema_for!(SocketMessage);
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::StaticConfigSchema => {
|
||||
let settings = SchemaSettings::default().with(|s| {
|
||||
@@ -1321,27 +1301,13 @@ impl WindowManager {
|
||||
let gen = settings.into_generator();
|
||||
let socket_message = gen.into_root_schema_for::<StaticConfig>();
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(schema.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::GenerateStaticConfig => {
|
||||
let config = serde_json::to_string_pretty(&StaticConfig::from(&*self))?;
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(config.as_bytes())?;
|
||||
}
|
||||
}
|
||||
reply.write_all(config.as_bytes())?;
|
||||
}
|
||||
SocketMessage::RemoveTitleBar(_, ref id) => {
|
||||
let mut identifiers = NO_TITLEBAR.lock();
|
||||
@@ -1352,135 +1318,34 @@ 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 { 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 mut rect = WindowsApi::window_rect(foreground_window.hwnd())?;
|
||||
rect.top -= self.invisible_borders.bottom;
|
||||
rect.bottom += self.invisible_borders.bottom;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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, &self.invisible_borders, false)?;
|
||||
}
|
||||
SocketMessage::TogglePause => {
|
||||
let is_paused = self.is_paused;
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
|
||||
if is_paused {
|
||||
border.hide()?;
|
||||
} else {
|
||||
let focused = self.focused_window()?;
|
||||
border.set_position(*focused, &self.invisible_borders, true)?;
|
||||
focused.focus(false)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleTiling | SocketMessage::WorkspaceTiling(..) => {
|
||||
let tiling_enabled = *self.focused_workspace_mut()?.tile();
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
|
||||
if tiling_enabled {
|
||||
let focused = self.focused_window()?;
|
||||
border.set_position(*focused, &self.invisible_borders, true)?;
|
||||
focused.focus(false)?;
|
||||
} else {
|
||||
border.hide()?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
let notification = Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: self.as_ref().into(),
|
||||
};
|
||||
|
||||
notify_subscribers(&serde_json::to_string(¬ification)?)?;
|
||||
border_manager::event_tx().send(border_manager::Notification)?;
|
||||
|
||||
tracing::info!("processed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
fn handle_initial_workspace_rules(
|
||||
&mut self,
|
||||
id: &String,
|
||||
@@ -1492,7 +1357,7 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
fn handle_definitive_workspace_rules(
|
||||
&mut self,
|
||||
id: &String,
|
||||
@@ -1504,7 +1369,7 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
pub fn handle_workspace_rules(
|
||||
&mut self,
|
||||
id: &String,
|
||||
@@ -1526,18 +1391,23 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, stream: UnixStream) -> Result<()> {
|
||||
let stream = BufReader::new(stream);
|
||||
for line in stream.lines() {
|
||||
pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream) -> Result<()> {
|
||||
let reader = BufReader::new(stream.try_clone()?);
|
||||
// TODO(raggi): while this processes more than one command, if there are
|
||||
// replies there is no clearly defined protocol for framing yet - it's
|
||||
// perhaps whole-json objects for now, but termination is signalled by
|
||||
// socket shutdown.
|
||||
for line in reader.lines() {
|
||||
let message = SocketMessage::from_str(&line?)?;
|
||||
|
||||
let mut wm = wm.lock();
|
||||
|
||||
if wm.is_paused {
|
||||
return match message {
|
||||
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
|
||||
Ok(wm.process_command(message)?)
|
||||
}
|
||||
SocketMessage::TogglePause
|
||||
| SocketMessage::State
|
||||
| SocketMessage::GlobalState
|
||||
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
|
||||
_ => {
|
||||
tracing::trace!("ignoring while paused");
|
||||
Ok(())
|
||||
@@ -1545,11 +1415,7 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, stream: UnixStream) ->
|
||||
};
|
||||
}
|
||||
|
||||
wm.process_command(message.clone())?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: wm.as_ref().into(),
|
||||
})?)?;
|
||||
wm.process_command(message.clone(), &mut stream)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1560,11 +1426,11 @@ pub fn read_commands_tcp(
|
||||
stream: &mut TcpStream,
|
||||
addr: &str,
|
||||
) -> Result<()> {
|
||||
let mut stream = BufReader::new(stream);
|
||||
let mut reader = BufReader::new(stream.try_clone()?);
|
||||
|
||||
loop {
|
||||
let mut buf = vec![0; 1024];
|
||||
match stream.read(&mut buf) {
|
||||
match reader.read(&mut buf) {
|
||||
Err(..) => {
|
||||
tracing::warn!("removing disconnected tcp client: {addr}");
|
||||
let mut connections = TCP_CONNECTIONS.lock();
|
||||
@@ -1584,9 +1450,10 @@ pub fn read_commands_tcp(
|
||||
|
||||
if wm.is_paused {
|
||||
return match message {
|
||||
SocketMessage::TogglePause | SocketMessage::State | SocketMessage::Stop => {
|
||||
Ok(wm.process_command(message)?)
|
||||
}
|
||||
SocketMessage::TogglePause
|
||||
| SocketMessage::State
|
||||
| SocketMessage::GlobalState
|
||||
| SocketMessage::Stop => Ok(wm.process_command(message, stream)?),
|
||||
_ => {
|
||||
tracing::trace!("ignoring while paused");
|
||||
Ok(())
|
||||
@@ -1594,11 +1461,7 @@ pub fn read_commands_tcp(
|
||||
};
|
||||
}
|
||||
|
||||
wm.process_command(message.clone())?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: wm.as_ref().into(),
|
||||
})?)?;
|
||||
wm.process_command(message.clone(), &mut *stream)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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 parking_lot::Mutex;
|
||||
|
||||
use komorebi_core::OperationDirection;
|
||||
@@ -12,22 +13,21 @@ 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::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::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::DATA_DIR;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
@@ -35,18 +35,17 @@ use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||
let receiver = wm.lock().incoming_events.lock().clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
std::thread::spawn(move || loop {
|
||||
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)
|
||||
}
|
||||
let receiver = wm.lock().incoming_events.clone();
|
||||
for event in receiver {
|
||||
match wm.lock().process_event(event) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!("{:?}", error)
|
||||
} else {
|
||||
tracing::error!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,12 +56,22 @@ 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 && !matches!(event, WindowManagerEvent::DisplayChange(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(virtual_desktop_id) = &self.virtual_desktop_id {
|
||||
if let Some(id) = current_virtual_desktop() {
|
||||
if id != *virtual_desktop_id {
|
||||
@@ -83,7 +92,7 @@ impl WindowManager {
|
||||
| WindowManagerEvent::MoveResizeEnd(_, window) => {
|
||||
self.reconcile_monitors()?;
|
||||
|
||||
let monitor_idx = self.monitor_idx_from_window(*window)
|
||||
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
|
||||
@@ -109,11 +118,15 @@ impl WindowManager {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let invisible_borders = self.invisible_borders;
|
||||
let offset = self.work_area_offset;
|
||||
|
||||
for (i, monitor) in self.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 {
|
||||
@@ -121,9 +134,13 @@ impl WindowManager {
|
||||
};
|
||||
|
||||
for (j, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||
if let WindowManagerEvent::FocusChange(_, window) = event {
|
||||
let _ = workspace.focus_changed(window.hwnd);
|
||||
}
|
||||
|
||||
let reaped_orphans = workspace.reap_orphans()?;
|
||||
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
|
||||
workspace.update(&work_area, offset, &invisible_borders)?;
|
||||
workspace.update(&work_area, offset, window_based_work_area_offset)?;
|
||||
tracing::info!(
|
||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||
reaped_orphans.0,
|
||||
@@ -146,16 +163,18 @@ impl WindowManager {
|
||||
|
||||
match event {
|
||||
WindowManagerEvent::Raise(window) => {
|
||||
window.raise();
|
||||
window.focus(false)?;
|
||||
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;
|
||||
@@ -169,7 +188,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) => {
|
||||
@@ -186,6 +205,7 @@ impl WindowManager {
|
||||
let title = &window.title()?;
|
||||
let exe_name = &window.exe()?;
|
||||
let class = &window.class()?;
|
||||
let path = &window.path()?;
|
||||
|
||||
// We don't want to purge windows that have been deliberately hidden by us, eg. when
|
||||
// they are not on the top of a container stack.
|
||||
@@ -194,9 +214,11 @@ impl WindowManager {
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&tray_and_multi_window_identifiers,
|
||||
®ex_identifiers,
|
||||
);
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if !window.is_window()
|
||||
|| should_act
|
||||
@@ -208,7 +230,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();
|
||||
@@ -216,6 +238,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()
|
||||
@@ -238,27 +262,47 @@ 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::event_tx().send(
|
||||
workspace_reconciliator::Notification {
|
||||
monitor_idx: i,
|
||||
workspace_idx: j,
|
||||
},
|
||||
)?;
|
||||
|
||||
needs_reconciliation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +311,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()
|
||||
@@ -278,46 +324,51 @@ 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;
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
if !workspace.contains_window(window.hwnd) {
|
||||
match behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
workspace.new_container_for_window(*window);
|
||||
self.update_focused_workspace(false)?;
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
workspace
|
||||
.focused_container_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused container"))?
|
||||
.add_window(*window);
|
||||
self.update_focused_workspace(true)?;
|
||||
if !workspace.contains_window(window.hwnd) && !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)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::MoveResizeStart(_, window) => {
|
||||
let monitor_idx = self.focused_monitor_idx();
|
||||
let workspace_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace_idx();
|
||||
let container_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace()
|
||||
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
|
||||
.focused_container_idx();
|
||||
if *self.focused_workspace()?.tile() {
|
||||
let monitor_idx = self.focused_monitor_idx();
|
||||
let workspace_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace_idx();
|
||||
let container_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace()
|
||||
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
|
||||
.focused_container_idx();
|
||||
|
||||
WindowsApi::bring_window_to_top(window.hwnd())?;
|
||||
WindowsApi::bring_window_to_top(window.hwnd())?;
|
||||
|
||||
self.pending_move_op = Option::from((monitor_idx, workspace_idx, container_idx));
|
||||
self.pending_move_op =
|
||||
Option::from((monitor_idx, workspace_idx, container_idx));
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::MoveResizeEnd(_, window) => {
|
||||
// We need this because if the event ends on a different monitor,
|
||||
@@ -331,47 +382,55 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?;
|
||||
|
||||
let new_window_behaviour = self.window_container_behaviour;
|
||||
let invisible_borders = self.invisible_borders;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if !workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.any(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
|
||||
let mut new_position = WindowsApi::window_rect(window.hwnd())?;
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
let new_position = WindowsApi::window_rect(window.hwnd())?;
|
||||
let old_position = *workspace
|
||||
.latest_layout()
|
||||
.get(focused_container_idx)
|
||||
// If the move was to another monitor with an empty workspace, the
|
||||
// workspace here will refer to that empty workspace, which won't
|
||||
// have any latest layout set. We fall back to a Default for Rect
|
||||
// which allows us to make a reasonable guess that the drag has taken
|
||||
// place across a monitor boundary to an empty workspace
|
||||
.unwrap_or(&Rect::default());
|
||||
|
||||
let old_position = *workspace
|
||||
.latest_layout()
|
||||
.get(focused_container_idx)
|
||||
// If the move was to another monitor with an empty workspace, the
|
||||
// workspace here will refer to that empty workspace, which won't
|
||||
// have any latest layout set. We fall back to a Default for Rect
|
||||
// which allows us to make a reasonable guess that the drag has taken
|
||||
// place across a monitor boundary to an empty workspace
|
||||
.unwrap_or(&Rect::default());
|
||||
// This will be true if we have moved to an empty workspace on another monitor
|
||||
let mut moved_across_monitors = old_position == Rect::default();
|
||||
if let Some((origin_monitor_idx, origin_workspace_idx, _)) = pending {
|
||||
// If we didn't move to another monitor with an empty workspace, it is
|
||||
// still possible that we moved to another monitor with a populated workspace
|
||||
if !moved_across_monitors {
|
||||
// So we'll check if the origin monitor index and the target monitor index
|
||||
// are different, if they are, we can set the override
|
||||
moved_across_monitors = origin_monitor_idx != target_monitor_idx;
|
||||
|
||||
// This will be true if we have moved to an empty workspace on another monitor
|
||||
let mut moved_across_monitors = old_position == Rect::default();
|
||||
if moved_across_monitors {
|
||||
// Want to make sure that we exclude unmanaged windows from cross-monitor
|
||||
// moves with a mouse, otherwise the currently focused idx container will
|
||||
// be moved when we just want to drag an unmanaged window
|
||||
let origin_workspace = self
|
||||
.monitors()
|
||||
.get(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("cannot get monitor idx"))?
|
||||
.workspaces()
|
||||
.get(origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
|
||||
|
||||
if let Some((origin_monitor_idx, _, _)) = pending {
|
||||
// If we didn't move to another monitor with an empty workspace, it is
|
||||
// still possible that we moved to another monitor with a populated workspace
|
||||
if !moved_across_monitors {
|
||||
// So we'll check if the origin monitor index and the target monitor index
|
||||
// are different, if they are, we can set the override
|
||||
moved_across_monitors = origin_monitor_idx != target_monitor_idx;
|
||||
let managed_window =
|
||||
origin_workspace.contains_managed_window(window.hwnd);
|
||||
|
||||
if !managed_window {
|
||||
moved_across_monitors = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust for the invisible borders
|
||||
new_position.left += invisible_borders.left;
|
||||
new_position.top += invisible_borders.top;
|
||||
new_position.right -= invisible_borders.right;
|
||||
new_position.bottom -= invisible_borders.bottom;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if workspace.contains_managed_window(window.hwnd) || moved_across_monitors {
|
||||
let resize = Rect {
|
||||
left: new_position.left - old_position.left,
|
||||
top: new_position.top - old_position.top,
|
||||
@@ -381,10 +440,14 @@ 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 is_move = moved_across_monitors
|
||||
|| resize.right == 0 && resize.bottom == 0
|
||||
|| resize.right.abs() == invisible_borders.right
|
||||
&& resize.bottom.abs() == invisible_borders.bottom;
|
||||
|| resize.right.abs() == right_bottom_constant
|
||||
&& resize.bottom.abs() == right_bottom_constant;
|
||||
|
||||
if is_move {
|
||||
tracing::info!("moving with mouse");
|
||||
@@ -432,11 +495,11 @@ 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
|
||||
@@ -447,11 +510,12 @@ impl WindowManager {
|
||||
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,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -460,11 +524,12 @@ 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,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -476,17 +541,17 @@ impl WindowManager {
|
||||
let mut ops = vec![];
|
||||
|
||||
macro_rules! resize_op {
|
||||
($coordinate:expr, $comparator:tt, $direction:expr) => {{
|
||||
let adjusted = $coordinate * 2;
|
||||
let sizing = if adjusted $comparator 0 {
|
||||
Sizing::Decrease
|
||||
} else {
|
||||
Sizing::Increase
|
||||
};
|
||||
($coordinate:expr, $comparator:tt, $direction:expr) => {{
|
||||
let adjusted = $coordinate * 2;
|
||||
let sizing = if adjusted $comparator 0 {
|
||||
Sizing::Decrease
|
||||
} else {
|
||||
Sizing::Increase
|
||||
};
|
||||
|
||||
($direction, sizing, adjusted.abs())
|
||||
}};
|
||||
}
|
||||
($direction, sizing, adjusted.abs())
|
||||
}};
|
||||
}
|
||||
|
||||
if resize.left != 0 {
|
||||
ops.push(resize_op!(resize.left, >, OperationDirection::Left));
|
||||
@@ -496,11 +561,14 @@ impl WindowManager {
|
||||
ops.push(resize_op!(resize.top, >, OperationDirection::Up));
|
||||
}
|
||||
|
||||
if resize.right != 0 && resize.left == 0 {
|
||||
let top_left_constant = BORDER_WIDTH.load(Ordering::SeqCst)
|
||||
+ BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
|
||||
if resize.right != 0 && resize.left == top_left_constant {
|
||||
ops.push(resize_op!(resize.right, <, OperationDirection::Right));
|
||||
}
|
||||
|
||||
if resize.bottom != 0 && resize.top == 0 {
|
||||
if resize.bottom != 0 && resize.top == top_left_constant {
|
||||
ops.push(resize_op!(resize.bottom, <, OperationDirection::Down));
|
||||
}
|
||||
|
||||
@@ -508,112 +576,21 @@ 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, true)?;
|
||||
}
|
||||
WindowManagerEvent::DisplayChange(..)
|
||||
| WindowManagerEvent::MouseCapture(..)
|
||||
| WindowManagerEvent::Cloak(..)
|
||||
| WindowManagerEvent::Uncloak(..) => {}
|
||||
| WindowManagerEvent::Cloak(..) => {}
|
||||
};
|
||||
|
||||
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::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 window = target_window;
|
||||
let mut rect = WindowsApi::window_rect(window.hwnd())?;
|
||||
rect.top -= self.invisible_borders.bottom;
|
||||
rect.bottom += self.invisible_borders.bottom;
|
||||
|
||||
let activate = BORDER_HIDDEN.load(Ordering::SeqCst);
|
||||
|
||||
WindowsApi::invalidate_border_rect()?;
|
||||
border.set_position(target_window, &self.invisible_borders, activate)?;
|
||||
|
||||
if activate {
|
||||
BORDER_HIDDEN.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
|
||||
if let WindowManagerEvent::Unmanage(window) = event {
|
||||
window.center(&self.focused_monitor_work_area()?, &invisible_borders)?;
|
||||
}
|
||||
|
||||
// If there are no more windows on the workspace, we shouldn't show the border window
|
||||
if self.focused_workspace()?.containers().is_empty() {
|
||||
let border = Border::from(BORDER_HWND.load(Ordering::SeqCst));
|
||||
border.hide()?;
|
||||
BORDER_HIDDEN.store(true, Ordering::SeqCst);
|
||||
window.center(&self.focused_monitor_work_area()?)?;
|
||||
}
|
||||
|
||||
tracing::trace!("updating list of known hwnds");
|
||||
@@ -636,10 +613,14 @@ impl WindowManager {
|
||||
.open(hwnd_json)?;
|
||||
|
||||
serde_json::to_writer_pretty(&file, &known_hwnds)?;
|
||||
notify_subscribers(&serde_json::to_string(&Notification {
|
||||
event: NotificationEvent::WindowManager(*event),
|
||||
|
||||
let notification = Notification {
|
||||
event: NotificationEvent::WindowManager(event),
|
||||
state: self.as_ref().into(),
|
||||
})?)?;
|
||||
};
|
||||
|
||||
notify_subscribers(&serde_json::to_string(¬ification)?)?;
|
||||
border_manager::event_tx().send(border_manager::Notification)?;
|
||||
|
||||
tracing::info!("processed: {}", event.window().to_string());
|
||||
Ok(())
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Ring<T> {
|
||||
elements: VecDeque<T>,
|
||||
focused: usize,
|
||||
|
||||
265
komorebi/src/stackbar.rs
Normal file
265
komorebi/src/stackbar.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
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::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::WINDOWS_BY_BAR_HWNDS;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Stackbar {
|
||||
pub(crate) hwnd: isize,
|
||||
}
|
||||
|
||||
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 };
|
||||
window.restore();
|
||||
if let Err(err) = window.focus(false) {
|
||||
tracing::error!("Stackbar focus error: HWND:{} {}", *win_hwnd, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(0),
|
||||
..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(0), 0, LWA_COLORKEY)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
|
||||
let mut msg = MSG::default();
|
||||
while GetMessageW(&mut msg, hwnd, 0, 0).into() {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
hwnd: hwnd_receiver.recv()?.0,
|
||||
})
|
||||
}
|
||||
|
||||
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));
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
use crate::border::Border;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::ZOrder;
|
||||
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::ring::Ring;
|
||||
@@ -6,15 +10,6 @@ use crate::window_manager::WindowManager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::workspace::Workspace;
|
||||
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_OVERFLOW_IDENTIFIERS;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
@@ -26,17 +21,28 @@ 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::WORKSPACE_RULES;
|
||||
use komorebi_core::StackbarMode;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use hotwatch::notify::DebouncedEvent;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use komorebi_core::config_generation::ApplicationConfiguration;
|
||||
use komorebi_core::config_generation::ApplicationConfigurationGenerator;
|
||||
use komorebi_core::config_generation::ApplicationOptions;
|
||||
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::ActiveWindowBorderStyle;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
@@ -62,34 +68,16 @@ use std::sync::Arc;
|
||||
use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Rgb {
|
||||
/// Red
|
||||
pub r: u32,
|
||||
/// Green
|
||||
pub g: u32,
|
||||
/// Blue
|
||||
pub b: u32,
|
||||
}
|
||||
|
||||
impl From<u32> for Rgb {
|
||||
fn from(value: u32) -> Self {
|
||||
Self {
|
||||
r: value & 0xff,
|
||||
g: value >> 8 & 0xff,
|
||||
b: value >> 16 & 0xff,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ActiveWindowBorderColours {
|
||||
/// Border colour when the container contains a single window
|
||||
pub single: Rgb,
|
||||
pub single: Option<Colour>,
|
||||
/// Border colour when the container contains multiple windows
|
||||
pub stack: Rgb,
|
||||
pub stack: Option<Colour>,
|
||||
/// Border colour when the container is in monocle mode
|
||||
pub monocle: Rgb,
|
||||
pub monocle: Option<Colour>,
|
||||
/// Border colour when the container is unfocused
|
||||
pub unfocused: Option<Colour>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -213,6 +201,12 @@ pub struct MonitorConfig {
|
||||
/// 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 {
|
||||
@@ -225,14 +219,16 @@ 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.20`
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.25`
|
||||
pub struct StaticConfig {
|
||||
/// Dimensions of Windows' own invisible borders; don't set these yourself unless you are told to
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub invisible_borders: Option<Rect>,
|
||||
/// Delta to resize windows by (default 50)
|
||||
@@ -256,18 +252,26 @@ pub struct StaticConfig {
|
||||
/// Path to applications.yaml from komorebi-application-specific-configurations (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub app_specific_configuration_path: Option<PathBuf>,
|
||||
/// Width of the active window border (default: 20)
|
||||
/// Width of the window border (default: 8)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_width: Option<i32>,
|
||||
/// Offset of the active window border (default: None)
|
||||
#[serde(alias = "active_window_border_width")]
|
||||
pub border_width: Option<i32>,
|
||||
/// Offset of the window border (default: -1)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_offset: Option<i32>,
|
||||
#[serde(alias = "active_window_border_offset")]
|
||||
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>,
|
||||
/// Active window border colours for different container types
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_colours: Option<ActiveWindowBorderColours>,
|
||||
/// Active window border style (default: System)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_style: Option<ActiveWindowBorderStyle>,
|
||||
/// Active window border z-order (default: System)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_window_border_z_order: Option<ZOrder>,
|
||||
/// Global default workspace padding (default: 10)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_workspace_padding: Option<i32>,
|
||||
@@ -277,10 +281,6 @@ pub struct StaticConfig {
|
||||
/// Monitor and workspace configurations
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitors: Option<Vec<MonitorConfig>>,
|
||||
/// DEPRECATED from v0.1.20: no longer required
|
||||
#[schemars(skip)]
|
||||
#[serde(skip_serializing)]
|
||||
pub alt_focus_hack: Option<bool>,
|
||||
/// Which Windows signal to use when hiding windows (default: minimize)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_hiding_behaviour: Option<HidingBehaviour>,
|
||||
@@ -289,40 +289,58 @@ pub struct StaticConfig {
|
||||
pub global_work_area_offset: Option<Rect>,
|
||||
/// Individual window floating rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_rules: Option<Vec<IdWithIdentifier>>,
|
||||
pub float_rules: Option<Vec<MatchingRule>>,
|
||||
/// Individual window force-manage rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manage_rules: Option<Vec<IdWithIdentifier>>,
|
||||
pub manage_rules: Option<Vec<MatchingRule>>,
|
||||
/// Identify border overflow applications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_overflow_applications: Option<Vec<IdWithIdentifier>>,
|
||||
pub border_overflow_applications: Option<Vec<MatchingRule>>,
|
||||
/// Identify tray and multi-window applications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_and_multi_window_applications: Option<Vec<IdWithIdentifier>>,
|
||||
pub tray_and_multi_window_applications: Option<Vec<MatchingRule>>,
|
||||
/// Identify applications that have the WS_EX_LAYERED extended window style
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layered_applications: Option<Vec<IdWithIdentifier>>,
|
||||
pub layered_applications: Option<Vec<MatchingRule>>,
|
||||
/// Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_name_change_applications: Option<Vec<IdWithIdentifier>>,
|
||||
pub object_name_change_applications: Option<Vec<MatchingRule>>,
|
||||
/// Set monitor index preferences
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitor_index_preferences: Option<HashMap<usize, Rect>>,
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StackbarConfig {
|
||||
/// Stackbar height
|
||||
pub height: Option<i32>,
|
||||
/// Stackbar mode
|
||||
pub mode: Option<StackbarMode>,
|
||||
/// Stackbar tab configuration options
|
||||
pub tabs: Option<TabsConfig>,
|
||||
}
|
||||
|
||||
impl From<&WindowManager> for StaticConfig {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn from(value: &WindowManager) -> Self {
|
||||
let default_invisible_borders = Rect {
|
||||
left: 7,
|
||||
top: 0,
|
||||
right: 14,
|
||||
bottom: 7,
|
||||
};
|
||||
|
||||
let mut monitors = vec![];
|
||||
for m in value.monitors() {
|
||||
monitors.push(MonitorConfig::from(m));
|
||||
@@ -373,30 +391,21 @@ impl From<&WindowManager> for StaticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
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: Rgb::from(BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)),
|
||||
stack: Rgb::from(if BORDER_COLOUR_STACK.load(Ordering::SeqCst) == 0 {
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
|
||||
} else {
|
||||
BORDER_COLOUR_STACK.load(Ordering::SeqCst)
|
||||
}),
|
||||
monocle: Rgb::from(if BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst) == 0 {
|
||||
BORDER_COLOUR_SINGLE.load(Ordering::SeqCst)
|
||||
} else {
|
||||
BORDER_COLOUR_MONOCLE.load(Ordering::SeqCst)
|
||||
}),
|
||||
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),
|
||||
)),
|
||||
})
|
||||
};
|
||||
|
||||
Self {
|
||||
invisible_borders: if value.invisible_borders == default_invisible_borders {
|
||||
None
|
||||
} else {
|
||||
Option::from(value.invisible_borders)
|
||||
},
|
||||
invisible_borders: None,
|
||||
resize_delta: Option::from(value.resize_delta),
|
||||
window_container_behaviour: Option::from(value.window_container_behaviour),
|
||||
cross_monitor_move_behaviour: Option::from(value.cross_monitor_move_behaviour),
|
||||
@@ -406,12 +415,14 @@ impl From<&WindowManager> for StaticConfig {
|
||||
focus_follows_mouse: value.focus_follows_mouse,
|
||||
mouse_follows_focus: Option::from(value.mouse_follows_focus),
|
||||
app_specific_configuration_path: None,
|
||||
active_window_border_width: Option::from(BORDER_WIDTH.load(Ordering::SeqCst)),
|
||||
active_window_border_offset: BORDER_OFFSET
|
||||
.lock()
|
||||
.map_or(None, |offset| Option::from(offset.left)),
|
||||
active_window_border: Option::from(BORDER_ENABLED.load(Ordering::SeqCst)),
|
||||
border_width: Option::from(border_manager::BORDER_WIDTH.load(Ordering::SeqCst)),
|
||||
border_offset: Option::from(border_manager::BORDER_OFFSET.load(Ordering::SeqCst)),
|
||||
active_window_border: Option::from(
|
||||
border_manager::BORDER_ENABLED.load(Ordering::SeqCst),
|
||||
),
|
||||
active_window_border_colours: border_colours,
|
||||
active_window_border_style: Option::from(*STYLE.lock()),
|
||||
active_window_border_z_order: Option::from(*Z_ORDER.lock()),
|
||||
default_workspace_padding: Option::from(
|
||||
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
|
||||
),
|
||||
@@ -419,7 +430,6 @@ impl From<&WindowManager> for StaticConfig {
|
||||
DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst),
|
||||
),
|
||||
monitors: Option::from(monitors),
|
||||
alt_focus_hack: None,
|
||||
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
|
||||
global_work_area_offset: value.work_area_offset,
|
||||
float_rules: None,
|
||||
@@ -430,6 +440,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
object_name_change_applications: None,
|
||||
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
|
||||
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()),
|
||||
stackbar: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,157 +471,94 @@ impl StaticConfig {
|
||||
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
self.active_window_border_width.map_or_else(
|
||||
|| {
|
||||
BORDER_WIDTH.store(20, Ordering::SeqCst);
|
||||
},
|
||||
|width| {
|
||||
BORDER_WIDTH.store(width, Ordering::SeqCst);
|
||||
},
|
||||
);
|
||||
self.active_window_border_offset.map_or_else(
|
||||
|| {
|
||||
let mut border_offset = BORDER_OFFSET.lock();
|
||||
*border_offset = None;
|
||||
},
|
||||
|offset| {
|
||||
let new_border_offset = Rect {
|
||||
left: offset,
|
||||
top: offset,
|
||||
right: offset * 2,
|
||||
bottom: offset * 2,
|
||||
};
|
||||
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);
|
||||
|
||||
let mut border_offset = BORDER_OFFSET.lock();
|
||||
*border_offset = Some(new_border_offset);
|
||||
},
|
||||
);
|
||||
if let Some(enabled) = &self.active_window_border {
|
||||
border_manager::BORDER_ENABLED.store(*enabled, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(colours) = &self.active_window_border_colours {
|
||||
BORDER_COLOUR_SINGLE.store(
|
||||
colours.single.r | (colours.single.g << 8) | (colours.single.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
BORDER_COLOUR_CURRENT.store(
|
||||
colours.single.r | (colours.single.g << 8) | (colours.single.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
BORDER_COLOUR_STACK.store(
|
||||
colours.stack.r | (colours.stack.g << 8) | (colours.stack.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
BORDER_COLOUR_MONOCLE.store(
|
||||
colours.monocle.r | (colours.monocle.g << 8) | (colours.monocle.b << 16),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
let active_window_border_style = self.active_window_border_style.unwrap_or_default();
|
||||
*STYLE.lock() = active_window_border_style;
|
||||
|
||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
let mut tray_and_multi_window_identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
let mut border_overflow_identifiers = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
let mut object_name_change_identifiers = OBJECT_NAME_CHANGE_ON_LAUNCH.lock();
|
||||
let mut layered_identifiers = LAYERED_WHITELIST.lock();
|
||||
|
||||
if let Some(float) = &mut self.float_rules {
|
||||
for identifier in float {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !float_identifiers.contains(identifier) {
|
||||
float_identifiers.push(identifier.clone());
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut self.float_rules {
|
||||
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
|
||||
if let Some(manage) = &mut self.manage_rules {
|
||||
for identifier in manage {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !manage_identifiers.contains(identifier) {
|
||||
manage_identifiers.push(identifier.clone());
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut self.manage_rules {
|
||||
populate_rules(rules, &mut manage_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &mut self.object_name_change_applications {
|
||||
for identifier in identifiers {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !object_name_change_identifiers.contains(identifier) {
|
||||
object_name_change_identifiers.push(identifier.clone());
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut self.object_name_change_applications {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut object_name_change_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &mut self.layered_applications {
|
||||
for identifier in identifiers {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !layered_identifiers.contains(identifier) {
|
||||
layered_identifiers.push(identifier.clone());
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut self.layered_applications {
|
||||
populate_rules(rules, &mut layered_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &mut self.border_overflow_applications {
|
||||
for identifier in identifiers {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !border_overflow_identifiers.contains(identifier) {
|
||||
border_overflow_identifiers.push(identifier.clone());
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut self.tray_and_multi_window_applications {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut tray_and_multi_window_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(identifiers) = &mut self.tray_and_multi_window_applications {
|
||||
for identifier in identifiers {
|
||||
if identifier.matching_strategy.is_none() {
|
||||
identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
if let Some(stackbar) = &self.stackbar {
|
||||
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(tabs) = &stackbar.tabs {
|
||||
if let Some(background) = &tabs.background {
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.store((*background).into(), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if !tray_and_multi_window_identifiers.contains(identifier) {
|
||||
tray_and_multi_window_identifiers.push(identifier.clone());
|
||||
if let Some(colour) = &tabs.focused_text {
|
||||
STACKBAR_FOCUSED_TEXT_COLOUR.store((*colour).into(), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if matches!(identifier.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&identifier.id)?;
|
||||
regex_identifiers.insert(identifier.id.clone(), re);
|
||||
}
|
||||
if let Some(colour) = &tabs.unfocused_text {
|
||||
STACKBAR_UNFOCUSED_TEXT_COLOUR.store((*colour).into(), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(width) = &tabs.width {
|
||||
STACKBAR_TAB_WIDTH.store(*width, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -621,121 +569,43 @@ impl StaticConfig {
|
||||
let asc = ApplicationConfigurationGenerator::load(&content)?;
|
||||
|
||||
for mut entry in asc {
|
||||
if let Some(float) = entry.float_identifiers {
|
||||
for f in float {
|
||||
let mut without_comment: IdWithIdentifier = f.into();
|
||||
if without_comment.matching_strategy.is_none() {
|
||||
without_comment.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !float_identifiers.contains(&without_comment) {
|
||||
float_identifiers.push(without_comment.clone());
|
||||
|
||||
if matches!(
|
||||
without_comment.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&without_comment.id)?;
|
||||
regex_identifiers.insert(without_comment.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(rules) = &mut entry.float_identifiers {
|
||||
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
if let Some(options) = entry.options {
|
||||
|
||||
if let Some(ref options) = entry.options {
|
||||
let options = options.clone();
|
||||
for o in options {
|
||||
match o {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !object_name_change_identifiers.contains(&entry.identifier) {
|
||||
object_name_change_identifiers.push(entry.identifier.clone());
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut object_name_change_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !layered_identifiers.contains(&entry.identifier) {
|
||||
layered_identifiers.push(entry.identifier.clone());
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !border_overflow_identifiers.contains(&entry.identifier) {
|
||||
border_overflow_identifiers.push(entry.identifier.clone());
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut layered_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !tray_and_multi_window_identifiers.contains(&entry.identifier) {
|
||||
tray_and_multi_window_identifiers
|
||||
.push(entry.identifier.clone());
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut tray_and_multi_window_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy =
|
||||
Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if !manage_identifiers.contains(&entry.identifier) {
|
||||
manage_identifiers.push(entry.identifier.clone());
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut manage_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {} // deprecated
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -748,7 +618,7 @@ impl StaticConfig {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn preload(
|
||||
path: &PathBuf,
|
||||
incoming: Arc<Mutex<Receiver<WindowManagerEvent>>>,
|
||||
incoming: Receiver<WindowManagerEvent>,
|
||||
) -> Result<WindowManager> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let mut value: Self = serde_json::from_str(&content)?;
|
||||
@@ -775,12 +645,6 @@ impl StaticConfig {
|
||||
incoming_events: incoming,
|
||||
command_listener: listener,
|
||||
is_paused: false,
|
||||
invisible_borders: value.invisible_borders.unwrap_or(Rect {
|
||||
left: 7,
|
||||
top: 0,
|
||||
right: 14,
|
||||
bottom: 7,
|
||||
}),
|
||||
virtual_desktop_id: current_virtual_desktop(),
|
||||
work_area_offset: value.global_work_area_offset,
|
||||
window_container_behaviour: value
|
||||
@@ -811,10 +675,10 @@ impl StaticConfig {
|
||||
|
||||
let bytes = SocketMessage::ReloadStaticConfiguration(path.clone()).as_bytes()?;
|
||||
|
||||
wm.hotwatch.watch(path, move |event| match event {
|
||||
wm.hotwatch.watch(path, move |event| match event.kind {
|
||||
// Editing in Notepad sends a NoticeWrite while editing in (Neo)Vim sends
|
||||
// a NoticeRemove, presumably because of the use of swap files?
|
||||
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::NoticeRemove(_) => {
|
||||
EventKind::Modify(_) | EventKind::Remove(_) => {
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
let mut stream =
|
||||
UnixStream::connect(socket).expect("could not connect to komorebi.sock");
|
||||
@@ -838,6 +702,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(
|
||||
@@ -866,12 +734,7 @@ 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()?;
|
||||
border_manager::BORDER_ENABLED.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -883,11 +746,25 @@ impl StaticConfig {
|
||||
|
||||
value.apply_globals()?;
|
||||
|
||||
let stackbar_mode = *STACKBAR_MODE.lock();
|
||||
|
||||
for m in wm.monitors_mut() {
|
||||
for w in m.workspaces_mut() {
|
||||
for c in w.containers_mut() {
|
||||
c.set_stackbar_mode(stackbar_mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(monitors) = value.monitors {
|
||||
for (i, monitor) in monitors.iter().enumerate() {
|
||||
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(
|
||||
@@ -915,20 +792,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(val) = value.invisible_borders {
|
||||
wm.invisible_borders = val;
|
||||
if let Some(enabled) = value.active_window_border {
|
||||
border_manager::BORDER_ENABLED.store(enabled, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(val) = value.window_container_behaviour {
|
||||
@@ -963,6 +828,76 @@ impl StaticConfig {
|
||||
|
||||
wm.focus_follows_mouse = value.focus_follows_mouse;
|
||||
|
||||
let monitor_count = wm.monitors().len();
|
||||
|
||||
for i in 0..monitor_count {
|
||||
wm.update_focused_workspace_by_monitor_idx(i)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_option(
|
||||
entry: &mut ApplicationConfiguration,
|
||||
identifiers: &mut Vec<MatchingRule>,
|
||||
regex_identifiers: &mut HashMap<String, Regex>,
|
||||
) -> Result<()> {
|
||||
if entry.identifier.matching_strategy.is_none() {
|
||||
entry.identifier.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
let rule = MatchingRule::Simple(entry.identifier.clone());
|
||||
|
||||
if !identifiers.contains(&rule) {
|
||||
identifiers.push(rule);
|
||||
|
||||
if matches!(
|
||||
entry.identifier.matching_strategy,
|
||||
Some(MatchingStrategy::Regex)
|
||||
) {
|
||||
let re = Regex::new(&entry.identifier.id)?;
|
||||
regex_identifiers.insert(entry.identifier.id.clone(), re);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn populate_rules(
|
||||
matching_rules: &mut Vec<MatchingRule>,
|
||||
identifiers: &mut Vec<MatchingRule>,
|
||||
regex_identifiers: &mut HashMap<String, Regex>,
|
||||
) -> Result<()> {
|
||||
for matching_rule in matching_rules {
|
||||
if !identifiers.contains(matching_rule) {
|
||||
match matching_rule {
|
||||
MatchingRule::Simple(simple) => {
|
||||
if simple.matching_strategy.is_none() {
|
||||
simple.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if matches!(simple.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&simple.id)?;
|
||||
regex_identifiers.insert(simple.id.clone(), re);
|
||||
}
|
||||
}
|
||||
MatchingRule::Composite(composite) => {
|
||||
for rule in composite {
|
||||
if rule.matching_strategy.is_none() {
|
||||
rule.matching_strategy = Option::from(MatchingStrategy::Legacy);
|
||||
}
|
||||
|
||||
if matches!(rule.matching_strategy, Some(MatchingStrategy::Regex)) {
|
||||
let re = Regex::new(&rule.id)?;
|
||||
regex_identifiers.insert(rule.id.clone(), re);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
identifiers.push(matching_rule.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,23 +4,21 @@ use std::convert::TryFrom;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::Result;
|
||||
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;
|
||||
use serde::Serializer;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use winput::press;
|
||||
use winput::release;
|
||||
use winput::Vk;
|
||||
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
@@ -30,8 +28,6 @@ use crate::styles::ExtendedWindowStyle;
|
||||
use crate::styles::WindowStyle;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::ALT_FOCUS_HACK;
|
||||
use crate::BORDER_OVERFLOW_IDENTIFIERS;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
@@ -42,9 +38,9 @@ use crate::PERMAIGNORE_CLASSES;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::WSL2_UI_PROCESSES;
|
||||
|
||||
#[derive(Debug, Clone, Copy, JsonSchema)]
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema)]
|
||||
pub struct Window {
|
||||
pub(crate) hwnd: isize,
|
||||
pub hwnd: isize,
|
||||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
@@ -100,24 +96,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()
|
||||
}
|
||||
@@ -128,7 +123,7 @@ impl Window {
|
||||
HWND(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn center(&mut self, work_area: &Rect, invisible_borders: &Rect) -> Result<()> {
|
||||
pub fn center(&self, work_area: &Rect) -> Result<()> {
|
||||
let half_width = work_area.right / 2;
|
||||
let half_weight = work_area.bottom / 2;
|
||||
|
||||
@@ -139,45 +134,31 @@ impl Window {
|
||||
right: half_width,
|
||||
bottom: half_weight,
|
||||
},
|
||||
invisible_borders,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_position(
|
||||
&mut self,
|
||||
layout: &Rect,
|
||||
invisible_borders: &Rect,
|
||||
top: bool,
|
||||
) -> Result<()> {
|
||||
let mut rect = *layout;
|
||||
|
||||
let border_overflows = BORDER_OVERFLOW_IDENTIFIERS.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
let title = &self.title()?;
|
||||
let class = &self.class()?;
|
||||
let exe_name = &self.exe()?;
|
||||
|
||||
let should_remove_border = !should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
&border_overflows,
|
||||
®ex_identifiers,
|
||||
);
|
||||
|
||||
if should_remove_border {
|
||||
// Remove the invisible borders
|
||||
rect.left -= invisible_borders.left;
|
||||
rect.top -= invisible_borders.top;
|
||||
rect.right += invisible_borders.right;
|
||||
rect.bottom += invisible_borders.bottom;
|
||||
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
|
||||
if WindowsApi::window_rect(self.hwnd())?.eq(layout) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rect = *layout;
|
||||
WindowsApi::position_window(self.hwnd(), &rect, top)
|
||||
}
|
||||
|
||||
pub fn is_maximized(self) -> bool {
|
||||
WindowsApi::is_zoomed(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn is_miminized(self) -> bool {
|
||||
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) {
|
||||
@@ -242,138 +223,21 @@ impl Window {
|
||||
WindowsApi::unmaximize_window(self.hwnd());
|
||||
}
|
||||
|
||||
pub fn raise(self) {
|
||||
// Attach komorebi thread to Window thread
|
||||
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
let current_thread_id = WindowsApi::current_thread_id();
|
||||
|
||||
// This can be allowed to fail if a window doesn't have a message queue or if a journal record
|
||||
// hook has been installed
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput#remarks
|
||||
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not attach to window thread input processing mechanism, but continuing execution of raise(): {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Raise Window to foreground
|
||||
match WindowsApi::set_foreground_window(self.hwnd()) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not set as foreground window, but continuing execution of raise(): {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 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 raise(): {}",
|
||||
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 raise(): {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn focus(self, mouse_follows_focus: bool) -> Result<()> {
|
||||
// Attach komorebi thread to Window thread
|
||||
let (_, window_thread_id) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
let current_thread_id = WindowsApi::current_thread_id();
|
||||
|
||||
// This can be allowed to fail if a window doesn't have a message queue or if a journal record
|
||||
// hook has been installed
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-attachthreadinput#remarks
|
||||
match WindowsApi::attach_thread_input(current_thread_id, window_thread_id, true) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"could not attach to window thread input processing mechanism, but continuing execution of focus(): {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Raise Window to foreground
|
||||
let mut foregrounded = false;
|
||||
let mut tried_resetting_foreground_access = false;
|
||||
let mut max_attempts = 10;
|
||||
|
||||
let hotkey_uses_alt = WindowsApi::alt_is_pressed();
|
||||
while !foregrounded && max_attempts > 0 {
|
||||
if ALT_FOCUS_HACK.load(Ordering::SeqCst) {
|
||||
press(Vk::Alt);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ALT_FOCUS_HACK.load(Ordering::SeqCst) && !hotkey_uses_alt {
|
||||
release(Vk::Alt);
|
||||
// If the target window is already focused, do nothing.
|
||||
if let Ok(ihwnd) = WindowsApi::foreground_window() {
|
||||
if HWND(ihwnd) == self.hwnd() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -413,6 +277,14 @@ impl Window {
|
||||
WindowsApi::window_text_w(self.hwnd())
|
||||
}
|
||||
|
||||
pub fn path(self) -> Result<String> {
|
||||
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
let handle = WindowsApi::process_handle(process_id)?;
|
||||
let path = WindowsApi::exe_path(handle);
|
||||
WindowsApi::close_process(handle)?;
|
||||
path
|
||||
}
|
||||
|
||||
pub fn exe(self) -> Result<String> {
|
||||
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
let handle = WindowsApi::process_handle(process_id)?;
|
||||
@@ -447,18 +319,27 @@ 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;
|
||||
|
||||
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;
|
||||
|
||||
@@ -471,13 +352,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)) = (self.title(), self.exe(), self.class()) {
|
||||
return Ok(window_is_eligible(&title, &exe_name, &class, &self.style()?, &self.ex_style()?, event));
|
||||
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) = (self.title(), self.exe(), self.class(), self.path()) {
|
||||
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(&title, &exe_name, &class, &path, style, ex_style, event, debug);
|
||||
debug.should_manage = eligible;
|
||||
return Ok(eligible);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -487,17 +383,42 @@ impl Window {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct RuleDebug {
|
||||
pub should_manage: bool,
|
||||
pub is_window: bool,
|
||||
pub has_title: bool,
|
||||
pub is_cloaked: bool,
|
||||
pub allow_cloaked: 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(
|
||||
title: &String,
|
||||
exe_name: &String,
|
||||
class: &String,
|
||||
path: &str,
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -505,42 +426,65 @@ 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 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 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 = {
|
||||
@@ -548,6 +492,10 @@ fn window_is_eligible(
|
||||
titlebars_removed.contains(exe_name)
|
||||
};
|
||||
|
||||
if exe_name.contains("firefox") {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
|
||||
&& !ex_style.contains(ExtendedWindowStyle::DLGMODALFRAME)
|
||||
// Get a lot of dupe events coming through that make the redrawing go crazy
|
||||
@@ -558,8 +506,13 @@ fn window_is_eligible(
|
||||
|| managed_override
|
||||
{
|
||||
return true;
|
||||
} else if event.is_some() {
|
||||
tracing::debug!("ignoring (exe: {}, title: {})", exe_name, title);
|
||||
} else if let Some(event) = event {
|
||||
tracing::debug!(
|
||||
"ignoring (exe: {}, title: {}, event: {})",
|
||||
exe_name,
|
||||
title,
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
false
|
||||
@@ -570,124 +523,290 @@ pub fn should_act(
|
||||
title: &str,
|
||||
exe_name: &str,
|
||||
class: &str,
|
||||
identifiers: &[IdWithIdentifier],
|
||||
path: &str,
|
||||
identifiers: &[MatchingRule],
|
||||
regex_identifiers: &HashMap<String, Regex>,
|
||||
) -> Option<MatchingRule> {
|
||||
let mut matching_rule = None;
|
||||
for rule in identifiers {
|
||||
match rule {
|
||||
MatchingRule::Simple(identifier) => {
|
||||
if should_act_individual(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
identifier,
|
||||
regex_identifiers,
|
||||
) {
|
||||
matching_rule = Some(rule.clone());
|
||||
};
|
||||
}
|
||||
MatchingRule::Composite(identifiers) => {
|
||||
let mut composite_results = vec![];
|
||||
for identifier in identifiers {
|
||||
composite_results.push(should_act_individual(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
identifier,
|
||||
regex_identifiers,
|
||||
));
|
||||
}
|
||||
|
||||
if composite_results.iter().all(|&x| x) {
|
||||
matching_rule = Some(rule.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matching_rule
|
||||
}
|
||||
|
||||
pub fn should_act_individual(
|
||||
title: &str,
|
||||
exe_name: &str,
|
||||
class: &str,
|
||||
path: &str,
|
||||
identifier: &IdWithIdentifier,
|
||||
regex_identifiers: &HashMap<String, Regex>,
|
||||
) -> bool {
|
||||
let mut should_act = false;
|
||||
for identifier in identifiers {
|
||||
match identifier.matching_strategy {
|
||||
None => {
|
||||
panic!("there is no matching strategy identified for this rule");
|
||||
}
|
||||
Some(MatchingStrategy::Legacy) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.starts_with(&identifier.id) || class.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Equals) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::StartsWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::EndsWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Contains) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Regex) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(title) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(class) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(exe_name) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
match identifier.matching_strategy {
|
||||
None => {
|
||||
panic!("there is no matching strategy identified for this rule");
|
||||
}
|
||||
Some(MatchingStrategy::Legacy) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.starts_with(&identifier.id) || class.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if path.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Equals) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if path.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::DoesNotEqual) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if !title.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if !class.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if !exe_name.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if !path.eq(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::StartsWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if path.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::DoesNotStartWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if !title.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if !class.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if !exe_name.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if !path.starts_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::EndsWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if path.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::DoesNotEndWith) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if !title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if !class.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if !exe_name.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if !path.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Contains) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if class.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if exe_name.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if path.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::DoesNotContain) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if !title.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if !class.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if !exe_name.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if !path.contains(&identifier.id) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MatchingStrategy::Regex) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(title) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Class => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(class) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Exe => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(exe_name) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationIdentifier::Path => {
|
||||
if let Some(re) = regex_identifiers.get(&identifier.id) {
|
||||
if re.is_match(path) {
|
||||
should_act = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
should_act
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::window::should_act;
|
||||
@@ -10,7 +11,7 @@ use crate::winevent::WinEvent;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, JsonSchema)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum WindowManagerEvent {
|
||||
Destroy(WinEvent, Window),
|
||||
@@ -27,6 +28,7 @@ pub enum WindowManagerEvent {
|
||||
Unmanage(Window),
|
||||
Raise(Window),
|
||||
DisplayChange(Window),
|
||||
ForceUpdate(Window),
|
||||
}
|
||||
|
||||
impl Display for WindowManagerEvent {
|
||||
@@ -77,6 +79,9 @@ impl Display for WindowManagerEvent {
|
||||
Self::DisplayChange(window) => {
|
||||
write!(f, "DisplayChange (Window: {window})")
|
||||
}
|
||||
Self::ForceUpdate(window) => {
|
||||
write!(f, "ForceUpdate (Window: {window})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +102,8 @@ impl WindowManagerEvent {
|
||||
| Self::Raise(window)
|
||||
| Self::Manage(window)
|
||||
| Self::DisplayChange(window)
|
||||
| Self::Unmanage(window) => window,
|
||||
| Self::Unmanage(window)
|
||||
| Self::ForceUpdate(window) => window,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,14 +144,17 @@ impl WindowManagerEvent {
|
||||
let title = &window.title().ok()?;
|
||||
let exe_name = &window.exe().ok()?;
|
||||
let class = &window.class().ok()?;
|
||||
let path = &window.path().ok()?;
|
||||
|
||||
let should_trigger = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&object_name_change_on_launch,
|
||||
®ex_identifiers,
|
||||
);
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if should_trigger {
|
||||
Option::from(Self::Show(winevent, window))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::convert::TryFrom;
|
||||
use std::ffi::c_void;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::mem::size_of;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::Error;
|
||||
@@ -22,6 +22,7 @@ use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
|
||||
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_EXTENDED_FRAME_BOUNDS;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_WINDOW_CORNER_PREFERENCE;
|
||||
use windows::Win32::Graphics::Dwm::DWMWCP_ROUND;
|
||||
use windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE;
|
||||
@@ -32,9 +33,10 @@ use windows::Win32::Graphics::Gdi::CreateSolidBrush;
|
||||
use windows::Win32::Graphics::Gdi::EnumDisplayDevicesW;
|
||||
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
|
||||
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::MonitorFromPoint;
|
||||
use windows::Win32::Graphics::Gdi::MonitorFromWindow;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::RoundRect;
|
||||
use windows::Win32::Graphics::Gdi::DISPLAY_DEVICEW;
|
||||
use windows::Win32::Graphics::Gdi::HBRUSH;
|
||||
use windows::Win32::Graphics::Gdi::HDC;
|
||||
@@ -44,9 +46,7 @@ use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
|
||||
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId;
|
||||
use windows::Win32::System::Threading::AttachThreadInput;
|
||||
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||
use windows::Win32::System::Threading::GetCurrentThreadId;
|
||||
use windows::Win32::System::Threading::OpenProcess;
|
||||
use windows::Win32::System::Threading::QueryFullProcessImageNameW;
|
||||
use windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
|
||||
@@ -58,13 +58,13 @@ use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||
use windows::Win32::UI::HiDpi::MDT_EFFECTIVE_DPI;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::SendInput;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::INPUT_MOUSE;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTDOWN;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTUP;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEINPUT;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::VK_LBUTTON;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::VK_MENU;
|
||||
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::BringWindowToTop;
|
||||
@@ -82,6 +82,7 @@ use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IsIconic;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IsWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IsZoomed;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::RegisterClassW;
|
||||
@@ -98,9 +99,7 @@ use windows::Win32::UI::WindowsAndMessaging::EDD_GET_DEVICE_INTERFACE_NAME;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_BOTTOM;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_NOTOPMOST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_TOPMOST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
|
||||
@@ -110,6 +109,9 @@ use windows::Win32::UI::WindowsAndMessaging::SPI_GETACTIVEWINDOWTRACKING;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_GETFOREGROUNDLOCKTIMEOUT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETACTIVEWINDOWTRACKING;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETFOREGROUNDLOCKTIMEOUT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_NOMOVE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_NOSIZE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SW_MAXIMIZE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SW_MINIMIZE;
|
||||
@@ -125,8 +127,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_MAXIMIZEBOX;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_MINIMIZEBOX;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
|
||||
|
||||
@@ -138,8 +139,7 @@ use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
use crate::set_window_position::SetWindowPosition;
|
||||
use crate::windows_callbacks;
|
||||
use crate::BORDER_HWND;
|
||||
use crate::TRANSPARENCY_COLOUR;
|
||||
use crate::Window;
|
||||
|
||||
pub enum WindowsResult<T, E> {
|
||||
Err(E),
|
||||
@@ -264,7 +264,7 @@ impl WindowsApi {
|
||||
}
|
||||
.ok()
|
||||
{
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!("enum_display_devices: {}", error);
|
||||
return Err(error.into());
|
||||
@@ -339,46 +339,65 @@ impl WindowsApi {
|
||||
unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0
|
||||
}
|
||||
|
||||
/// position window resizes the target window to the given layout, adjusting
|
||||
/// the layout to account for any window shadow borders (the window painted
|
||||
/// region will match layout on completion).
|
||||
pub fn position_window(hwnd: HWND, layout: &Rect, top: bool) -> Result<()> {
|
||||
let flags = SetWindowPosition::NO_ACTIVATE
|
||||
let mut flags = SetWindowPosition::NO_ACTIVATE
|
||||
| SetWindowPosition::NO_SEND_CHANGING
|
||||
| SetWindowPosition::NO_COPY_BITS
|
||||
| SetWindowPosition::FRAME_CHANGED;
|
||||
|
||||
let position = if top { HWND_TOPMOST } else { HWND_BOTTOM };
|
||||
Self::set_window_pos(hwnd, layout, position, flags.bits())
|
||||
// If the request is to place the window on top, then HWND_TOP will take
|
||||
// effect, otherwise pass NO_Z_ORDER that will cause set_window_pos to
|
||||
// ignore the z-order paramter.
|
||||
if !top {
|
||||
flags |= SetWindowPosition::NO_Z_ORDER;
|
||||
}
|
||||
|
||||
let shadow_rect = Self::shadow_rect(hwnd).unwrap_or_default();
|
||||
let rect = Rect {
|
||||
left: layout.left + shadow_rect.left,
|
||||
top: layout.top + shadow_rect.top,
|
||||
right: layout.right + shadow_rect.right,
|
||||
bottom: layout.bottom + shadow_rect.bottom,
|
||||
};
|
||||
|
||||
// Note: earlier code had set HWND_TOPMOST here, but we should not do
|
||||
// that. HWND_TOPMOST is a sticky z-order change, rather than a regular
|
||||
// z-order reordering. Programs will use TOPMOST themselves to do things
|
||||
// such as making sure that their tool windows or dialog pop-ups are
|
||||
// above their main window. If any such windows are unmanaged, they must
|
||||
// still remian topmost, so we set HWND_TOP here, which will cause the
|
||||
// managed window to come to the front, but if the managed window has a
|
||||
// child that is TOPMOST it will still be rendered above, in the proper
|
||||
// order expected by the application. It's also important to understand
|
||||
// that TOPMOST is somewhat viral, in that when you set a window to
|
||||
// TOPMOST all of its owned windows are also made TOPMOST.
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos#remarks
|
||||
Self::set_window_pos(hwnd, &rect, HWND_TOP, flags.bits())
|
||||
}
|
||||
|
||||
pub fn bring_window_to_top(hwnd: HWND) -> Result<()> {
|
||||
unsafe { BringWindowToTop(hwnd) }.process()
|
||||
}
|
||||
|
||||
// Raise the window to the top of the Z order, but do not activate or focus
|
||||
// it. Use raise_and_focus_window to activate and focus a window.
|
||||
pub fn raise_window(hwnd: HWND) -> Result<()> {
|
||||
let flags = SetWindowPosition::NO_MOVE;
|
||||
let flags = SetWindowPosition::NO_MOVE | SetWindowPosition::NO_ACTIVATE;
|
||||
|
||||
let position = HWND_TOPMOST;
|
||||
let position = HWND_TOP;
|
||||
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
|
||||
}
|
||||
|
||||
pub fn position_border_window(hwnd: HWND, layout: &Rect, activate: bool) -> Result<()> {
|
||||
let flags = if activate {
|
||||
SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE
|
||||
} else {
|
||||
SetWindowPosition::NO_ACTIVATE
|
||||
};
|
||||
|
||||
let position = HWND_NOTOPMOST;
|
||||
pub fn set_border_pos(hwnd: HWND, layout: &Rect, position: HWND) -> Result<()> {
|
||||
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
|
||||
Self::set_window_pos(hwnd, layout, position, flags.bits())
|
||||
}
|
||||
|
||||
pub fn hide_border_window(hwnd: HWND) -> Result<()> {
|
||||
let flags = SetWindowPosition::HIDE_WINDOW;
|
||||
|
||||
let position = HWND_BOTTOM;
|
||||
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
|
||||
}
|
||||
|
||||
pub fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
|
||||
/// set_window_pos calls SetWindowPos without any accounting for Window decorations.
|
||||
fn set_window_pos(hwnd: HWND, layout: &Rect, position: HWND, flags: u32) -> Result<()> {
|
||||
unsafe {
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
@@ -393,7 +412,7 @@ impl WindowsApi {
|
||||
.process()
|
||||
}
|
||||
|
||||
fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
|
||||
pub fn show_window(hwnd: HWND, command: SHOW_WINDOW_CMD) {
|
||||
// BOOL is returned but does not signify whether or not the operation was succesful
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
|
||||
unsafe { ShowWindow(hwnd, command) };
|
||||
@@ -434,8 +453,31 @@ impl WindowsApi {
|
||||
unsafe { GetForegroundWindow() }.process()
|
||||
}
|
||||
|
||||
pub fn set_foreground_window(hwnd: HWND) -> Result<()> {
|
||||
unsafe { SetForegroundWindow(hwnd) }.ok().process()
|
||||
pub fn raise_and_focus_window(hwnd: HWND) -> Result<()> {
|
||||
let event = [INPUT {
|
||||
r#type: INPUT_MOUSE,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
unsafe {
|
||||
// Send an input event to our own process first so that we pass the
|
||||
// foreground lock check
|
||||
SendInput(&event, size_of::<INPUT>() as i32);
|
||||
// Error ignored, as the operation is not always necessary.
|
||||
let _ = SetWindowPos(
|
||||
hwnd,
|
||||
HWND_TOP,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW,
|
||||
)
|
||||
.process();
|
||||
SetForegroundWindow(hwnd)
|
||||
}
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -452,6 +494,16 @@ impl WindowsApi {
|
||||
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.process()
|
||||
}
|
||||
|
||||
pub fn alt_tab_windows() -> Result<Vec<Window>> {
|
||||
let mut hwnds = vec![];
|
||||
Self::enum_windows(
|
||||
Some(windows_callbacks::alt_tab_windows),
|
||||
&mut hwnds as *mut Vec<Window> as isize,
|
||||
)?;
|
||||
|
||||
Ok(hwnds)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn top_visible_window() -> Result<isize> {
|
||||
let hwnd = Self::top_window()?;
|
||||
@@ -470,11 +522,56 @@ impl WindowsApi {
|
||||
|
||||
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
|
||||
let mut rect = unsafe { std::mem::zeroed() };
|
||||
unsafe { GetWindowRect(hwnd, &mut rect) }.process()?;
|
||||
|
||||
Ok(Rect::from(rect))
|
||||
if Self::dwm_get_window_attribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &mut rect).is_ok() {
|
||||
// TODO(raggi): once we declare DPI awareness, we will need to scale the rect.
|
||||
// let window_scale = unsafe { GetDpiForWindow(hwnd) };
|
||||
// let system_scale = unsafe { GetDpiForSystem() };
|
||||
// Ok(Rect::from(rect).scale(system_scale.try_into()?, window_scale.try_into()?))
|
||||
Ok(Rect::from(rect))
|
||||
} else {
|
||||
unsafe { GetWindowRect(hwnd, &mut rect) }.process()?;
|
||||
Ok(Rect::from(rect))
|
||||
}
|
||||
}
|
||||
|
||||
/// shadow_rect computes the offset of the shadow position of the window to
|
||||
/// the window painted region. The four values in the returned Rect can be
|
||||
/// added to a position rect to compute a size for set_window_pos that will
|
||||
/// fill the target area, ignoring shadows.
|
||||
fn shadow_rect(hwnd: HWND) -> Result<Rect> {
|
||||
let window_rect = Self::window_rect(hwnd)?;
|
||||
|
||||
let mut srect = Default::default();
|
||||
unsafe { GetWindowRect(hwnd, &mut srect) }.process()?;
|
||||
let shadow_rect = Rect::from(srect);
|
||||
|
||||
Ok(Rect {
|
||||
left: shadow_rect.left - window_rect.left,
|
||||
top: shadow_rect.top - window_rect.top,
|
||||
right: shadow_rect.right - window_rect.right,
|
||||
bottom: shadow_rect.bottom - window_rect.bottom,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn round_rect(hdc: HDC, rect: &Rect, border_radius: i32) {
|
||||
unsafe {
|
||||
RoundRect(
|
||||
hdc,
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.right,
|
||||
rect.bottom,
|
||||
border_radius,
|
||||
border_radius,
|
||||
);
|
||||
}
|
||||
}
|
||||
pub fn rectangle(hdc: HDC, rect: &Rect) {
|
||||
unsafe {
|
||||
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
|
||||
unsafe { SetCursorPos(x, y) }.process()
|
||||
}
|
||||
@@ -510,10 +607,6 @@ impl WindowsApi {
|
||||
(process_id, thread_id)
|
||||
}
|
||||
|
||||
pub fn current_thread_id() -> u32 {
|
||||
unsafe { GetCurrentThreadId() }
|
||||
}
|
||||
|
||||
pub fn current_process_id() -> u32 {
|
||||
unsafe { GetCurrentProcessId() }
|
||||
}
|
||||
@@ -531,16 +624,6 @@ impl WindowsApi {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attach_thread_input(thread_id: u32, target_thread_id: u32, attach: bool) -> Result<()> {
|
||||
unsafe { AttachThreadInput(thread_id, target_thread_id, attach) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn set_focus(hwnd: HWND) -> Result<()> {
|
||||
unsafe { SetFocus(hwnd) }.process().map(|_| ())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn set_window_long_ptr_w(
|
||||
hwnd: HWND,
|
||||
@@ -677,6 +760,10 @@ impl WindowsApi {
|
||||
unsafe { IsIconic(hwnd) }.into()
|
||||
}
|
||||
|
||||
pub fn is_zoomed(hwnd: HWND) -> bool {
|
||||
unsafe { IsZoomed(hwnd) }.into()
|
||||
}
|
||||
|
||||
pub fn monitor_info_w(hmonitor: HMONITOR) -> Result<MONITORINFOEXW> {
|
||||
let mut ex_info = MONITORINFOEXW::default();
|
||||
ex_info.monitorInfo.cbSize = u32::try_from(std::mem::size_of::<MONITORINFOEXW>())?;
|
||||
@@ -844,10 +931,10 @@ impl WindowsApi {
|
||||
pub fn create_border_window(name: PCWSTR, instance: HMODULE) -> Result<isize> {
|
||||
unsafe {
|
||||
let hwnd = CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED,
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
|
||||
name,
|
||||
name,
|
||||
WS_POPUP | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
|
||||
WS_POPUP | WS_SYSMENU,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
@@ -858,7 +945,7 @@ impl WindowsApi {
|
||||
None,
|
||||
);
|
||||
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(TRANSPARENCY_COLOUR), 0, LWA_COLORKEY)?;
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
|
||||
|
||||
hwnd
|
||||
}
|
||||
@@ -895,12 +982,6 @@ impl WindowsApi {
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn invalidate_border_rect() -> Result<()> {
|
||||
unsafe { InvalidateRect(HWND(BORDER_HWND.load(Ordering::SeqCst)), None, false) }
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn alt_is_pressed() -> bool {
|
||||
let state = unsafe { GetKeyState(i32::from(VK_MENU.0)) };
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
@@ -908,6 +989,13 @@ impl WindowsApi {
|
||||
actual != 0
|
||||
}
|
||||
|
||||
pub fn lbutton_is_pressed() -> bool {
|
||||
let state = unsafe { GetKeyState(i32::from(VK_LBUTTON.0)) };
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
let actual = (state as u16) & 0x8000;
|
||||
actual != 0
|
||||
}
|
||||
|
||||
pub fn left_click() -> u32 {
|
||||
let inputs = [
|
||||
INPUT {
|
||||
|
||||
@@ -1,49 +1,34 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::Ordering;
|
||||
use widestring::U16CStr;
|
||||
|
||||
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::RECT;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Gdi::BeginPaint;
|
||||
use windows::Win32::Graphics::Gdi::CreatePen;
|
||||
use windows::Win32::Graphics::Gdi::EndPaint;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::SelectObject;
|
||||
use windows::Win32::Graphics::Gdi::ValidateRect;
|
||||
use windows::Win32::Graphics::Gdi::HDC;
|
||||
use windows::Win32::Graphics::Gdi::HMONITOR;
|
||||
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_ICONVERTICALSPACING;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_SETTINGCHANGE;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
use crate::window::RuleDebug;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener::WINEVENT_CALLBACK_CHANNEL;
|
||||
use crate::BORDER_COLOUR_CURRENT;
|
||||
use crate::BORDER_RECT;
|
||||
use crate::BORDER_WIDTH;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::winevent_listener;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::TRANSPARENCY_COLOUR;
|
||||
|
||||
pub extern "system" fn valid_display_monitors(
|
||||
hmonitor: HMONITOR,
|
||||
@@ -146,12 +131,17 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let is_visible = WindowsApi::is_window_visible(hwnd);
|
||||
let is_window = WindowsApi::is_window(hwnd);
|
||||
let is_minimized = WindowsApi::is_iconic(hwnd);
|
||||
let is_maximized = WindowsApi::is_zoomed(hwnd);
|
||||
|
||||
if is_visible && is_window && !is_minimized {
|
||||
let window = Window { hwnd: hwnd.0 };
|
||||
|
||||
if let Ok(should_manage) = window.should_manage(None) {
|
||||
if let Ok(should_manage) = window.should_manage(None, &mut RuleDebug::default()) {
|
||||
if should_manage {
|
||||
if is_maximized {
|
||||
WindowsApi::restore_window(hwnd);
|
||||
}
|
||||
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(window);
|
||||
containers.push_back(container);
|
||||
@@ -162,6 +152,26 @@ pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
true.into()
|
||||
}
|
||||
|
||||
pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
|
||||
|
||||
let is_visible = WindowsApi::is_window_visible(hwnd);
|
||||
let is_window = WindowsApi::is_window(hwnd);
|
||||
let is_minimized = WindowsApi::is_iconic(hwnd);
|
||||
|
||||
if is_visible && is_window && !is_minimized {
|
||||
let window = Window { hwnd: hwnd.0 };
|
||||
|
||||
if let Ok(should_manage) = window.should_manage(None, &mut RuleDebug::default()) {
|
||||
if should_manage {
|
||||
windows.push(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true.into()
|
||||
}
|
||||
|
||||
pub extern "system" fn win_event_hook(
|
||||
_h_win_event_hook: HWINEVENTHOOK,
|
||||
event: u32,
|
||||
@@ -178,57 +188,18 @@ pub extern "system" fn win_event_hook(
|
||||
|
||||
let window = Window { hwnd: hwnd.0 };
|
||||
|
||||
let winevent = unsafe { ::std::mem::transmute(event) };
|
||||
let winevent = match WinEvent::try_from(event) {
|
||||
Ok(event) => event,
|
||||
Err(_) => return,
|
||||
};
|
||||
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
|
||||
None => return,
|
||||
Some(event) => event,
|
||||
};
|
||||
|
||||
if let Ok(should_manage) = window.should_manage(Option::from(event_type)) {
|
||||
if should_manage {
|
||||
WINEVENT_CALLBACK_CHANNEL
|
||||
.lock()
|
||||
.0
|
||||
.send(event_type)
|
||||
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub extern "system" fn border_window(
|
||||
window: HWND,
|
||||
message: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_PAINT => {
|
||||
let border_rect = *BORDER_RECT.lock();
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
let hpen = CreatePen(
|
||||
PS_SOLID,
|
||||
BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
COLORREF(BORDER_COLOUR_CURRENT.load(Ordering::SeqCst)),
|
||||
);
|
||||
let hbrush = WindowsApi::create_solid_brush(TRANSPARENCY_COLOUR);
|
||||
|
||||
SelectObject(hdc, hpen);
|
||||
SelectObject(hdc, hbrush);
|
||||
Rectangle(hdc, 0, 0, border_rect.right, border_rect.bottom);
|
||||
EndPaint(window, &ps);
|
||||
ValidateRect(window, None);
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(window, message, wparam, lparam),
|
||||
}
|
||||
}
|
||||
winevent_listener::event_tx()
|
||||
.send(event_type)
|
||||
.expect("could not send message on winevent_listener::event_tx");
|
||||
}
|
||||
|
||||
pub extern "system" fn hidden_window(
|
||||
@@ -241,11 +212,9 @@ pub extern "system" fn hidden_window(
|
||||
match message {
|
||||
WM_DISPLAYCHANGE => {
|
||||
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
|
||||
WINEVENT_CALLBACK_CHANNEL
|
||||
.lock()
|
||||
.0
|
||||
winevent_listener::event_tx()
|
||||
.send(event_type)
|
||||
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
|
||||
.expect("could not send message on winevent_listener::event_tx");
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
@@ -256,11 +225,9 @@ pub extern "system" fn hidden_window(
|
||||
|| wparam.0 as u32 == SPI_ICONVERTICALSPACING.0
|
||||
{
|
||||
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
|
||||
WINEVENT_CALLBACK_CHANNEL
|
||||
.lock()
|
||||
.0
|
||||
winevent_listener::event_tx()
|
||||
.send(event_type)
|
||||
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
|
||||
.expect("could not send message on winevent_listener::event_tx");
|
||||
}
|
||||
LRESULT(0)
|
||||
}
|
||||
@@ -269,11 +236,9 @@ pub extern "system" fn hidden_window(
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
if wparam.0 as u32 == DBT_DEVNODES_CHANGED {
|
||||
let event_type = WindowManagerEvent::DisplayChange(Window { hwnd: window.0 });
|
||||
WINEVENT_CALLBACK_CHANNEL
|
||||
.lock()
|
||||
.0
|
||||
winevent_listener::event_tx()
|
||||
.send(event_type)
|
||||
.expect("could not send message on WINEVENT_CALLBACK_CHANNEL");
|
||||
.expect("could not send message on winevent_listener::event_tx");
|
||||
}
|
||||
LRESULT(0)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![allow(clippy::use_self)]
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_AIA_END;
|
||||
@@ -88,7 +89,7 @@ use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Display, JsonSchema)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Display, JsonSchema)]
|
||||
#[repr(u32)]
|
||||
#[allow(dead_code)]
|
||||
pub enum WinEvent {
|
||||
@@ -177,3 +178,100 @@ pub enum WinEvent {
|
||||
UiaPropIdSEnd = EVENT_UIA_PROPID_END,
|
||||
UiaPropIdStart = EVENT_UIA_PROPID_START,
|
||||
}
|
||||
|
||||
impl TryFrom<u32> for WinEvent {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
EVENT_AIA_END => Ok(Self::AiaEnd),
|
||||
EVENT_AIA_START => Ok(Self::AiaStart),
|
||||
EVENT_CONSOLE_CARET => Ok(Self::ConsoleCaret),
|
||||
EVENT_CONSOLE_END => Ok(Self::ConsoleEnd),
|
||||
EVENT_CONSOLE_END_APPLICATION => Ok(Self::ConsoleEndApplication),
|
||||
EVENT_CONSOLE_LAYOUT => Ok(Self::ConsoleLayout),
|
||||
EVENT_CONSOLE_START_APPLICATION => Ok(Self::ConsoleStartApplication),
|
||||
EVENT_CONSOLE_UPDATE_REGION => Ok(Self::ConsoleUpdateRegion),
|
||||
EVENT_CONSOLE_UPDATE_SCROLL => Ok(Self::ConsoleUpdateScroll),
|
||||
EVENT_CONSOLE_UPDATE_SIMPLE => Ok(Self::ConsoleUpdateSimple),
|
||||
EVENT_OBJECT_ACCELERATORCHANGE => Ok(Self::ObjectAcceleratorChange),
|
||||
EVENT_OBJECT_CLOAKED => Ok(Self::ObjectCloaked),
|
||||
EVENT_OBJECT_CONTENTSCROLLED => Ok(Self::ObjectContentScrolled),
|
||||
EVENT_OBJECT_CREATE => Ok(Self::ObjectCreate),
|
||||
EVENT_OBJECT_DEFACTIONCHANGE => Ok(Self::ObjectDefActionChange),
|
||||
EVENT_OBJECT_DESCRIPTIONCHANGE => Ok(Self::ObjectDescriptionChange),
|
||||
EVENT_OBJECT_DESTROY => Ok(Self::ObjectDestroy),
|
||||
EVENT_OBJECT_DRAGCANCEL => Ok(Self::ObjectDragCancel),
|
||||
EVENT_OBJECT_DRAGCOMPLETE => Ok(Self::ObjectDragComplete),
|
||||
EVENT_OBJECT_DRAGDROPPED => Ok(Self::ObjectDragDropped),
|
||||
EVENT_OBJECT_DRAGENTER => Ok(Self::ObjectDragEnter),
|
||||
EVENT_OBJECT_DRAGLEAVE => Ok(Self::ObjectDragLeave),
|
||||
EVENT_OBJECT_DRAGSTART => Ok(Self::ObjectDragStart),
|
||||
EVENT_OBJECT_END => Ok(Self::ObjectEnd),
|
||||
EVENT_OBJECT_FOCUS => Ok(Self::ObjectFocus),
|
||||
EVENT_OBJECT_HELPCHANGE => Ok(Self::ObjectHelpChange),
|
||||
EVENT_OBJECT_HIDE => Ok(Self::ObjectHide),
|
||||
EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED => Ok(Self::ObjectHostedObjectsInvalidated),
|
||||
EVENT_OBJECT_IME_CHANGE => Ok(Self::ObjectImeChange),
|
||||
EVENT_OBJECT_IME_HIDE => Ok(Self::ObjectImeHide),
|
||||
EVENT_OBJECT_IME_SHOW => Ok(Self::ObjectImeShow),
|
||||
EVENT_OBJECT_INVOKED => Ok(Self::ObjectInvoked),
|
||||
EVENT_OBJECT_LIVEREGIONCHANGED => Ok(Self::ObjectLiveRegionChanged),
|
||||
EVENT_OBJECT_LOCATIONCHANGE => Ok(Self::ObjectLocationChange),
|
||||
EVENT_OBJECT_NAMECHANGE => Ok(Self::ObjectNameChange),
|
||||
EVENT_OBJECT_PARENTCHANGE => Ok(Self::ObjectParentChange),
|
||||
EVENT_OBJECT_REORDER => Ok(Self::ObjectReorder),
|
||||
EVENT_OBJECT_SELECTION => Ok(Self::ObjectSelection),
|
||||
EVENT_OBJECT_SELECTIONADD => Ok(Self::ObjectSelectionAdd),
|
||||
EVENT_OBJECT_SELECTIONREMOVE => Ok(Self::ObjectSelectionRemove),
|
||||
EVENT_OBJECT_SELECTIONWITHIN => Ok(Self::ObjectSelectionWithin),
|
||||
EVENT_OBJECT_SHOW => Ok(Self::ObjectShow),
|
||||
EVENT_OBJECT_STATECHANGE => Ok(Self::ObjectStateChange),
|
||||
EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED => {
|
||||
Ok(Self::ObjectTextEditConversionTargetChanged)
|
||||
}
|
||||
EVENT_OBJECT_TEXTSELECTIONCHANGED => Ok(Self::ObjectTextSelectionChanged),
|
||||
EVENT_OBJECT_UNCLOAKED => Ok(Self::ObjectUncloaked),
|
||||
EVENT_OBJECT_VALUECHANGE => Ok(Self::ObjectValueChange),
|
||||
EVENT_OEM_DEFINED_END => Ok(Self::OemDefinedEnd),
|
||||
EVENT_OEM_DEFINED_START => Ok(Self::OemDefinedStart),
|
||||
EVENT_SYSTEM_ALERT => Ok(Self::SystemAlert),
|
||||
EVENT_SYSTEM_ARRANGMENTPREVIEW => Ok(Self::SystemArrangementPreview),
|
||||
EVENT_SYSTEM_CAPTUREEND => Ok(Self::SystemCaptureEnd),
|
||||
EVENT_SYSTEM_CAPTURESTART => Ok(Self::SystemCaptureStart),
|
||||
EVENT_SYSTEM_CONTEXTHELPEND => Ok(Self::SystemContextHelpEnd),
|
||||
EVENT_SYSTEM_CONTEXTHELPSTART => Ok(Self::SystemContextHelpStart),
|
||||
EVENT_SYSTEM_DESKTOPSWITCH => Ok(Self::SystemDesktopSwitch),
|
||||
EVENT_SYSTEM_DIALOGEND => Ok(Self::SystemDialogEnd),
|
||||
EVENT_SYSTEM_DIALOGSTART => Ok(Self::SystemDialogStart),
|
||||
EVENT_SYSTEM_DRAGDROPEND => Ok(Self::SystemDragDropEnd),
|
||||
EVENT_SYSTEM_DRAGDROPSTART => Ok(Self::SystemDragDropStart),
|
||||
EVENT_SYSTEM_END => Ok(Self::SystemEnd),
|
||||
EVENT_SYSTEM_FOREGROUND => Ok(Self::SystemForeground),
|
||||
EVENT_SYSTEM_IME_KEY_NOTIFICATION => Ok(Self::SystemImeKeyNotification),
|
||||
EVENT_SYSTEM_MENUEND => Ok(Self::SystemMenuEnd),
|
||||
EVENT_SYSTEM_MENUPOPUPEND => Ok(Self::SystemMenuPopupEnd),
|
||||
EVENT_SYSTEM_MENUPOPUPSTART => Ok(Self::SystemMenuPopupStart),
|
||||
EVENT_SYSTEM_MENUSTART => Ok(Self::SystemMenuStart),
|
||||
EVENT_SYSTEM_MINIMIZEEND => Ok(Self::SystemMinimizeEnd),
|
||||
EVENT_SYSTEM_MINIMIZESTART => Ok(Self::SystemMinimizeStart),
|
||||
EVENT_SYSTEM_MOVESIZEEND => Ok(Self::SystemMoveSizeEnd),
|
||||
EVENT_SYSTEM_MOVESIZESTART => Ok(Self::SystemMoveSizeStart),
|
||||
EVENT_SYSTEM_SCROLLINGEND => Ok(Self::SystemScrollingEnd),
|
||||
EVENT_SYSTEM_SCROLLINGSTART => Ok(Self::SystemScrollingStart),
|
||||
EVENT_SYSTEM_SOUND => Ok(Self::SystemSound),
|
||||
EVENT_SYSTEM_SWITCHEND => Ok(Self::SystemSwitchEnd),
|
||||
EVENT_SYSTEM_SWITCHER_APPDROPPED => Ok(Self::SystemSwitcherAppDropped),
|
||||
EVENT_SYSTEM_SWITCHER_APPGRABBED => Ok(Self::SystemSwitcherAppGrabbed),
|
||||
EVENT_SYSTEM_SWITCHER_APPOVERTARGET => Ok(Self::SystemSwitcherAppOverTarget),
|
||||
EVENT_SYSTEM_SWITCHER_CANCELLED => Ok(Self::SystemSwitcherCancelled),
|
||||
EVENT_SYSTEM_SWITCHSTART => Ok(Self::SystemSwitchStart),
|
||||
EVENT_UIA_EVENTID_END => Ok(Self::UiaEventIdSEnd),
|
||||
EVENT_UIA_EVENTID_START => Ok(Self::UiaEventIdStart),
|
||||
EVENT_UIA_PROPID_END => Ok(Self::UiaPropIdSEnd),
|
||||
EVENT_UIA_PROPID_START => Ok(Self::UiaPropIdStart),
|
||||
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +1,62 @@
|
||||
use std::sync::atomic::AtomicIsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::Accessibility::SetWinEventHook;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PeekMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_MAX;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_MIN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PM_REMOVE;
|
||||
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_callbacks;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref WINEVENT_CALLBACK_CHANNEL: Arc<Mutex<(Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>)>> =
|
||||
Arc::new(Mutex::new(crossbeam_channel::unbounded()));
|
||||
}
|
||||
static CHANNEL: OnceLock<(Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>)> =
|
||||
OnceLock::new();
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WinEventListener {
|
||||
hook: Arc<AtomicIsize>,
|
||||
outgoing_events: Arc<Mutex<Sender<WindowManagerEvent>>>,
|
||||
}
|
||||
static EVENT_PUMP: OnceLock<std::thread::JoinHandle<()>> = OnceLock::new();
|
||||
|
||||
pub fn new(outgoing: Arc<Mutex<Sender<WindowManagerEvent>>>) -> WinEventListener {
|
||||
WinEventListener {
|
||||
hook: Arc::new(AtomicIsize::new(0)),
|
||||
outgoing_events: outgoing,
|
||||
}
|
||||
}
|
||||
|
||||
impl WinEventListener {
|
||||
pub fn start(self) {
|
||||
let hook = self.hook.clone();
|
||||
let outgoing = self.outgoing_events.lock().clone();
|
||||
|
||||
std::thread::spawn(move || unsafe {
|
||||
let hook_ref = SetWinEventHook(
|
||||
EVENT_MIN,
|
||||
EVENT_MAX,
|
||||
None,
|
||||
Some(windows_callbacks::win_event_hook),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
hook.store(hook_ref.0, Ordering::SeqCst);
|
||||
|
||||
// The code in the callback doesn't work in its own loop, needs to be within
|
||||
// the MessageLoop callback for the winevent callback to even fire
|
||||
MessageLoop::start(10, |_msg| {
|
||||
if let Ok(event) = WINEVENT_CALLBACK_CHANNEL.lock().1.try_recv() {
|
||||
match outgoing.send(event) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct MessageLoop;
|
||||
|
||||
impl MessageLoop {
|
||||
pub fn start(sleep: u64, cb: impl Fn(Option<MSG>) -> bool) {
|
||||
Self::start_with_sleep(sleep, cb);
|
||||
}
|
||||
|
||||
fn start_with_sleep(sleep: u64, cb: impl Fn(Option<MSG>) -> bool) {
|
||||
let mut msg: MSG = MSG::default();
|
||||
loop {
|
||||
let mut value: Option<MSG> = None;
|
||||
pub fn start() {
|
||||
EVENT_PUMP.get_or_init(|| {
|
||||
std::thread::spawn(move || {
|
||||
unsafe {
|
||||
if !bool::from(!PeekMessageW(&mut msg, HWND(0), 0, 0, PM_REMOVE)) {
|
||||
SetWinEventHook(
|
||||
EVENT_MIN,
|
||||
EVENT_MAX,
|
||||
None,
|
||||
Some(windows_callbacks::win_event_hook),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
|
||||
loop {
|
||||
let mut msg: MSG = MSG::default();
|
||||
unsafe {
|
||||
if !GetMessageW(&mut msg, HWND(0), 0, 0).as_bool() {
|
||||
tracing::info!("windows event processing shutdown");
|
||||
break;
|
||||
};
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
|
||||
value = Some(msg);
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(sleep));
|
||||
|
||||
if !cb(value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn channel() -> &'static (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) {
|
||||
CHANNEL.get_or_init(crossbeam_channel::unbounded)
|
||||
}
|
||||
|
||||
pub fn event_tx() -> Sender<WindowManagerEvent> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
pub fn event_rx() -> Receiver<WindowManagerEvent> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use getset::Getters;
|
||||
use getset::MutGetters;
|
||||
use getset::Setters;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::Axis;
|
||||
@@ -19,6 +20,8 @@ use komorebi_core::Layout;
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::static_config::WorkspaceConfig;
|
||||
@@ -30,20 +33,24 @@ use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::STACKBAR_TAB_HEIGHT;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
#[derive(
|
||||
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
|
||||
)]
|
||||
pub struct Workspace {
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
name: Option<String>,
|
||||
containers: Ring<Container>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
monocle_container: Option<Container>,
|
||||
#[serde(skip_serializing)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
monocle_container_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
maximized_window: Option<Window>,
|
||||
#[serde(skip_serializing)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
maximized_window_restore_idx: Option<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub")]
|
||||
@@ -58,7 +65,6 @@ pub struct Workspace {
|
||||
workspace_padding: Option<i32>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
container_padding: Option<i32>,
|
||||
#[serde(skip_serializing)]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
latest_layout: Vec<Rect>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
@@ -125,6 +131,8 @@ impl Workspace {
|
||||
}
|
||||
|
||||
self.set_layout_rules(all_rules);
|
||||
|
||||
self.tile = true;
|
||||
}
|
||||
|
||||
if let Some(layout_rules) = &config.custom_layout_rules {
|
||||
@@ -133,60 +141,73 @@ impl Workspace {
|
||||
let rule = CustomLayout::from_path(pathbuf)?;
|
||||
rules.push((*count, Layout::Custom(rule)));
|
||||
}
|
||||
|
||||
self.tile = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
for container in self.containers_mut() {
|
||||
for window in container.windows_mut() {
|
||||
pub fn hide(&mut self, omit: Option<isize>) {
|
||||
for window in self.floating_windows_mut().iter_mut().rev() {
|
||||
let mut should_hide = omit.is_none();
|
||||
|
||||
if !should_hide {
|
||||
if let Some(omit) = omit {
|
||||
if omit != window.hwnd {
|
||||
should_hide = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_hide {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
for container in self.containers_mut() {
|
||||
container.hide(omit)
|
||||
}
|
||||
|
||||
if let Some(window) = self.maximized_window() {
|
||||
window.hide();
|
||||
}
|
||||
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
for window in container.windows_mut() {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
window.hide();
|
||||
container.hide(omit)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restore(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
||||
let idx = self.focused_container_idx();
|
||||
let mut to_focus = None;
|
||||
|
||||
for (i, container) in self.containers_mut().iter_mut().enumerate() {
|
||||
if let Some(window) = container.focused_window_mut() {
|
||||
window.restore();
|
||||
|
||||
if idx == i {
|
||||
to_focus = Option::from(*window);
|
||||
}
|
||||
}
|
||||
|
||||
container.restore();
|
||||
}
|
||||
|
||||
if let Some(window) = self.maximized_window() {
|
||||
window.maximize();
|
||||
for container in self.containers_mut() {
|
||||
container.restore();
|
||||
}
|
||||
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
for window in container.windows_mut() {
|
||||
window.restore();
|
||||
}
|
||||
container.restore();
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
window.restore();
|
||||
}
|
||||
|
||||
if let Some(container) = self.focused_container_mut() {
|
||||
container.focus_window(container.focused_window_idx());
|
||||
}
|
||||
|
||||
// Do this here to make sure that an error doesn't stop the restoration of other windows
|
||||
// Maximised windows should always be drawn at the top of the Z order
|
||||
if let Some(window) = to_focus {
|
||||
@@ -201,15 +222,18 @@ impl Workspace {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
work_area: &Rect,
|
||||
offset: Option<Rect>,
|
||||
invisible_borders: &Rect,
|
||||
work_area_offset: Option<Rect>,
|
||||
window_based_work_area_offset: (isize, Option<Rect>),
|
||||
) -> Result<()> {
|
||||
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (window_based_work_area_offset_limit, window_based_work_area_offset) =
|
||||
window_based_work_area_offset;
|
||||
|
||||
let container_padding = self.container_padding();
|
||||
let mut adjusted_work_area = offset.map_or_else(
|
||||
let mut adjusted_work_area = work_area_offset.map_or_else(
|
||||
|| *work_area,
|
||||
|offset| {
|
||||
let mut with_offset = *work_area;
|
||||
@@ -222,7 +246,22 @@ impl Workspace {
|
||||
},
|
||||
);
|
||||
|
||||
adjusted_work_area.add_padding(self.workspace_padding());
|
||||
if self.containers().len() <= window_based_work_area_offset_limit as usize {
|
||||
adjusted_work_area = window_based_work_area_offset.map_or_else(
|
||||
|| adjusted_work_area,
|
||||
|offset| {
|
||||
let mut with_offset = adjusted_work_area;
|
||||
with_offset.left += offset.left;
|
||||
with_offset.top += offset.top;
|
||||
with_offset.right -= offset.right;
|
||||
with_offset.bottom -= offset.bottom;
|
||||
|
||||
with_offset
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
adjusted_work_area.add_padding(self.workspace_padding().unwrap_or_default());
|
||||
|
||||
self.enforce_resize_constraints();
|
||||
|
||||
@@ -244,11 +283,19 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
let managed_maximized_window = self.maximized_window().is_some();
|
||||
|
||||
if *self.tile() {
|
||||
if let Some(container) = self.monocle_container_mut() {
|
||||
if let Some(window) = container.focused_window_mut() {
|
||||
adjusted_work_area.add_padding(container_padding);
|
||||
window.set_position(&adjusted_work_area, invisible_borders, true)?;
|
||||
adjusted_work_area.add_padding(container_padding.unwrap_or_default());
|
||||
{
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
adjusted_work_area.add_padding(border_offset);
|
||||
let width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
adjusted_work_area.add_padding(width);
|
||||
}
|
||||
window.set_position(&adjusted_work_area, true)?;
|
||||
};
|
||||
} else if let Some(window) = self.maximized_window_mut() {
|
||||
window.maximize();
|
||||
@@ -268,16 +315,64 @@ impl Workspace {
|
||||
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
let no_titlebar = NO_TITLEBAR.lock().clone();
|
||||
|
||||
let windows = self.visible_windows_mut();
|
||||
for (i, window) in windows.into_iter().enumerate() {
|
||||
if let (Some(window), Some(layout)) = (window, layouts.get(i)) {
|
||||
let focused_hwnd = self
|
||||
.focused_container()
|
||||
.ok_or_else(|| anyhow!("couldn't find a focused container"))?
|
||||
.focused_window()
|
||||
.ok_or_else(|| anyhow!("couldn't find a focused window"))?
|
||||
.hwnd;
|
||||
|
||||
let container_padding = self.container_padding().unwrap_or(0);
|
||||
let containers = self.containers_mut();
|
||||
|
||||
for (i, container) in containers.iter_mut().enumerate() {
|
||||
container.renew_stackbar();
|
||||
|
||||
let container_windows = container.windows().clone();
|
||||
let container_stackbar = container.stackbar().clone();
|
||||
|
||||
if let (Some(window), Some(layout)) =
|
||||
(container.focused_window_mut(), layouts.get(i))
|
||||
{
|
||||
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
|
||||
window.remove_title_bar()?;
|
||||
} else if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
window.set_position(layout, invisible_borders, false)?;
|
||||
// If a window has been unmaximized via toggle-maximize, this block
|
||||
// will make sure that it is unmaximized via restore_window
|
||||
if window.is_maximized() && !managed_maximized_window {
|
||||
WindowsApi::restore_window(window.hwnd());
|
||||
}
|
||||
|
||||
let mut rect = *layout;
|
||||
{
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
rect.add_padding(border_offset);
|
||||
|
||||
let width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
rect.add_padding(width);
|
||||
}
|
||||
|
||||
if let Some(stackbar) = container_stackbar {
|
||||
if stackbar
|
||||
.set_position(
|
||||
&stackbar.get_position_from_container_layout(layout),
|
||||
false,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
stackbar.update(&container_windows, focused_hwnd)?;
|
||||
let tab_height = STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst);
|
||||
let total_height = tab_height + container_padding;
|
||||
|
||||
rect.top += total_height;
|
||||
rect.bottom -= total_height;
|
||||
}
|
||||
}
|
||||
|
||||
window.set_position(&rect, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,6 +389,33 @@ impl Workspace {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// focus_changed performs updates in response to the fact that a focus
|
||||
// change event has occurred. The focus change is assumed to be valid, and
|
||||
// should not result in a new focus change - the intent here is to update
|
||||
// focus-reactive elements, such as the stackbar.
|
||||
pub fn focus_changed(&mut self, hwnd: isize) -> Result<()> {
|
||||
if !self.tile() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let containers = self.containers_mut();
|
||||
|
||||
for container in containers.iter_mut() {
|
||||
let container_windows = container.windows().clone();
|
||||
let container_topbar = container.stackbar().clone();
|
||||
|
||||
if let Some(idx) = container.idx_for_window(hwnd) {
|
||||
container.focus_window(idx);
|
||||
container.restore();
|
||||
}
|
||||
|
||||
if let Some(stackbar) = container_topbar {
|
||||
stackbar.update(&container_windows, hwnd)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reap_orphans(&mut self) -> Result<(usize, usize)> {
|
||||
let mut hwnds = vec![];
|
||||
let mut floating_hwnds = vec![];
|
||||
@@ -352,7 +474,18 @@ impl Workspace {
|
||||
.idx_for_window(hwnd)
|
||||
.ok_or_else(|| anyhow!("there is no window"))?;
|
||||
|
||||
let mut should_load = false;
|
||||
|
||||
if container.focused_window_idx() != window_idx {
|
||||
should_load = true
|
||||
}
|
||||
|
||||
container.focus_window(window_idx);
|
||||
|
||||
if should_load {
|
||||
container.load_focused_window();
|
||||
}
|
||||
|
||||
self.focus_container(container_idx);
|
||||
|
||||
Ok(())
|
||||
@@ -769,6 +902,10 @@ impl Workspace {
|
||||
if container.windows().is_empty() {
|
||||
self.containers_mut().remove(focused_idx);
|
||||
self.resize_dimensions_mut().remove(focused_idx);
|
||||
|
||||
if focused_idx == self.containers().len() && focused_idx != 0 {
|
||||
self.focus_container(focused_idx - 1);
|
||||
}
|
||||
} else {
|
||||
container.load_focused_window();
|
||||
}
|
||||
@@ -784,6 +921,17 @@ impl Workspace {
|
||||
fn enforce_resize_constraints(&mut self) {
|
||||
match self.layout {
|
||||
Layout::Default(DefaultLayout::BSP) => self.enforce_resize_constraints_for_bsp(),
|
||||
Layout::Default(DefaultLayout::Columns) => self.enforce_resize_for_columns(),
|
||||
Layout::Default(DefaultLayout::Rows) => self.enforce_resize_for_rows(),
|
||||
Layout::Default(DefaultLayout::VerticalStack) => {
|
||||
self.enforce_resize_for_vertical_stack();
|
||||
}
|
||||
Layout::Default(DefaultLayout::RightMainVerticalStack) => {
|
||||
self.enforce_resize_for_right_vertical_stack();
|
||||
}
|
||||
Layout::Default(DefaultLayout::HorizontalStack) => {
|
||||
self.enforce_resize_for_horizontal_stack();
|
||||
}
|
||||
Layout::Default(DefaultLayout::UltrawideVerticalStack) => {
|
||||
self.enforce_resize_for_ultrawide();
|
||||
}
|
||||
@@ -817,6 +965,146 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_for_columns(&mut self) {
|
||||
let resize_dimensions = self.resize_dimensions_mut();
|
||||
match resize_dimensions.len() {
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
let len = resize_dimensions.len();
|
||||
for (i, rect) in resize_dimensions.iter_mut().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
rect.top = 0;
|
||||
rect.bottom = 0;
|
||||
|
||||
if i == 0 {
|
||||
rect.left = 0;
|
||||
}
|
||||
if i == len - 1 {
|
||||
rect.right = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_for_rows(&mut self) {
|
||||
let resize_dimensions = self.resize_dimensions_mut();
|
||||
match resize_dimensions.len() {
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
let len = resize_dimensions.len();
|
||||
for (i, rect) in resize_dimensions.iter_mut().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
rect.left = 0;
|
||||
rect.right = 0;
|
||||
|
||||
if i == 0 {
|
||||
rect.top = 0;
|
||||
}
|
||||
if i == len - 1 {
|
||||
rect.bottom = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_for_vertical_stack(&mut self) {
|
||||
let resize_dimensions = self.resize_dimensions_mut();
|
||||
match resize_dimensions.len() {
|
||||
// Single window can not be resized at all
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
// Zero is actually on the left
|
||||
if let Some(mut left) = resize_dimensions[0] {
|
||||
left.top = 0;
|
||||
left.bottom = 0;
|
||||
left.left = 0;
|
||||
}
|
||||
|
||||
// Handle stack on the right
|
||||
let stack_size = resize_dimensions[1..].len();
|
||||
for (i, rect) in resize_dimensions[1..].iter_mut().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
// No containers can resize to the right
|
||||
rect.right = 0;
|
||||
|
||||
// First container in stack cant resize up
|
||||
if i == 0 {
|
||||
rect.top = 0;
|
||||
} else if i == stack_size - 1 {
|
||||
// Last cant be resized to the bottom
|
||||
rect.bottom = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_for_right_vertical_stack(&mut self) {
|
||||
let resize_dimensions = self.resize_dimensions_mut();
|
||||
match resize_dimensions.len() {
|
||||
// Single window can not be resized at all
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
// Zero is actually on the right
|
||||
if let Some(mut left) = resize_dimensions[1] {
|
||||
left.top = 0;
|
||||
left.bottom = 0;
|
||||
left.right = 0;
|
||||
}
|
||||
|
||||
// Handle stack on the right
|
||||
let stack_size = resize_dimensions[1..].len();
|
||||
for (i, rect) in resize_dimensions[1..].iter_mut().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
// No containers can resize to the left
|
||||
rect.left = 0;
|
||||
|
||||
// First container in stack cant resize up
|
||||
if i == 0 {
|
||||
rect.top = 0;
|
||||
} else if i == stack_size - 1 {
|
||||
// Last cant be resized to the bottom
|
||||
rect.bottom = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_for_horizontal_stack(&mut self) {
|
||||
let resize_dimensions = self.resize_dimensions_mut();
|
||||
match resize_dimensions.len() {
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
if let Some(mut left) = resize_dimensions[0] {
|
||||
left.top = 0;
|
||||
left.left = 0;
|
||||
left.right = 0;
|
||||
}
|
||||
|
||||
let stack_size = resize_dimensions[1..].len();
|
||||
for (i, rect) in resize_dimensions[1..].iter_mut().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
rect.bottom = 0;
|
||||
|
||||
if i == 0 {
|
||||
rect.left = 0;
|
||||
}
|
||||
if i == stack_size - 1 {
|
||||
rect.right = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_for_ultrawide(&mut self) {
|
||||
let resize_dimensions = self.resize_dimensions_mut();
|
||||
match resize_dimensions.len() {
|
||||
@@ -917,7 +1205,7 @@ impl Workspace {
|
||||
.ok_or_else(|| anyhow!("there is no monocle container"))?;
|
||||
|
||||
let container = container.clone();
|
||||
if restore_idx > self.containers().len() - 1 {
|
||||
if restore_idx >= self.containers().len() {
|
||||
self.containers_mut()
|
||||
.resize(restore_idx, Container::default());
|
||||
}
|
||||
@@ -1111,7 +1399,7 @@ impl Workspace {
|
||||
vec
|
||||
}
|
||||
|
||||
fn focus_previous_container(&mut self) {
|
||||
pub fn focus_previous_container(&mut self) {
|
||||
let focused_idx = self.focused_container_idx();
|
||||
|
||||
if focused_idx != 0 {
|
||||
|
||||
116
komorebi/src/workspace_reconciliator.rs
Normal file
116
komorebi/src/workspace_reconciliator.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::WindowManager;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Notification {
|
||||
pub monitor_idx: usize,
|
||||
pub workspace_idx: usize,
|
||||
}
|
||||
|
||||
pub static ALT_TAB_HWND: AtomicCell<Option<isize>> = AtomicCell::new(None);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ALT_TAB_HWND_INSTANT: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
|
||||
}
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
|
||||
}
|
||||
|
||||
pub fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
pub fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
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) => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::error!("restarting failed thread: {:?}", error)
|
||||
} else {
|
||||
tracing::error!("restarting failed thread: {}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
let arc = wm.clone();
|
||||
|
||||
for notification in receiver {
|
||||
tracing::info!("running reconciliation");
|
||||
|
||||
let mut wm = wm.lock();
|
||||
let focused_monitor_idx = wm.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
wm.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
let updated_pair = (notification.monitor_idx, notification.workspace_idx);
|
||||
|
||||
if focused_pair != updated_pair {
|
||||
wm.focus_monitor(notification.monitor_idx)?;
|
||||
let mouse_follows_focus = wm.mouse_follows_focus;
|
||||
|
||||
if let Some(monitor) = wm.focused_monitor_mut() {
|
||||
monitor.focus_workspace(notification.workspace_idx)?;
|
||||
monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
}
|
||||
|
||||
// Drop our lock on the window manager state here to not slow down updates
|
||||
drop(wm);
|
||||
|
||||
// Check if there was an alt-tab across workspaces in the last second
|
||||
if let Some(hwnd) = ALT_TAB_HWND.load() {
|
||||
if ALT_TAB_HWND_INSTANT
|
||||
.lock()
|
||||
.elapsed()
|
||||
.lt(&Duration::from_secs(1))
|
||||
{
|
||||
// Sleep for 100 millis to let other events pass
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
tracing::info!("focusing alt-tabbed window");
|
||||
|
||||
// Take a new lock on the wm and try to focus the container with
|
||||
// the recorded HWND from the alt-tab
|
||||
let mut wm = arc.lock();
|
||||
if let Ok(workspace) = wm.focused_workspace_mut() {
|
||||
// Regardless of if this fails, we need to get past this part
|
||||
// to unblock the border manager below
|
||||
let _ = workspace.focus_container_by_window(hwnd);
|
||||
}
|
||||
|
||||
// Unblock the border manager
|
||||
ALT_TAB_HWND.store(None);
|
||||
// Send a notification to the border manager to update the borders
|
||||
border_manager::event_tx().send(border_manager::Notification)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic-no-console"
|
||||
version = "0.1.21"
|
||||
version = "0.1.26-dev.0"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
||||
categories = ["cli", "tiling-window-manager", "windows"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
; Generated by komorebic.exe
|
||||
#Requires AutoHotkey v2.0.2
|
||||
|
||||
Start(ffm, await_configuration, tcp_port) {
|
||||
RunWait("komorebic.exe start " ffm " --await-configuration " await_configuration " --tcp-port " tcp_port, , "Hide")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.21"
|
||||
version = "0.1.26-dev.0"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||
categories = ["cli", "tiling-window-manager", "windows"]
|
||||
@@ -13,23 +13,27 @@ edition = "2021"
|
||||
[dependencies]
|
||||
derive-ahk = { path = "../derive-ahk" }
|
||||
komorebi-core = { path = "../komorebi-core" }
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
|
||||
clap = { version = "4", features = ["derive", "wrap_help"] }
|
||||
color-eyre = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
fs-tail = "0.1"
|
||||
heck = "0.4"
|
||||
heck = "0.5"
|
||||
lazy_static = "1"
|
||||
miette = { version = "5", features = ["fancy"] }
|
||||
miette = { version = "7", features = ["fancy"] }
|
||||
paste = "1"
|
||||
powershell_script = "1.0"
|
||||
reqwest = { version = "0.11", features = ["blocking"] }
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9"
|
||||
sysinfo = "0.30"
|
||||
sysinfo = { workspace = true }
|
||||
thiserror = "1"
|
||||
uds_windows = "1"
|
||||
which = "5"
|
||||
windows = { workspace = true }
|
||||
which = "6"
|
||||
windows = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
|
||||
8
komorebic/build.rs
Normal file
8
komorebic/build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
if std::fs::metadata("applications.yaml").is_err() {
|
||||
let applications_yaml = reqwest::blocking::get(
|
||||
"https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.yaml"
|
||||
).unwrap().text().unwrap();
|
||||
std::fs::write("applications.yaml", applications_yaml).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@ use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::Shutdown;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
@@ -20,6 +21,7 @@ use clap::ValueEnum;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::Result;
|
||||
use dirs::data_local_dir;
|
||||
use fs_tail::TailedFile;
|
||||
use heck::ToKebabCase;
|
||||
use komorebi_core::resolve_home_path;
|
||||
@@ -29,7 +31,6 @@ use miette::Report;
|
||||
use miette::SourceOffset;
|
||||
use miette::SourceSpan;
|
||||
use paste::paste;
|
||||
use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
use which::which;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
@@ -113,7 +114,7 @@ trait AhkFunction {
|
||||
struct ConfigurationError {
|
||||
message: String,
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
src: NamedSource<String>,
|
||||
#[label("This bit here")]
|
||||
bad_bit: SourceSpan,
|
||||
}
|
||||
@@ -477,6 +478,14 @@ pub struct SendToMonitorWorkspace {
|
||||
target_workspace: usize,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
pub struct MoveToMonitorWorkspace {
|
||||
/// Target monitor index (zero-indexed)
|
||||
target_monitor: usize,
|
||||
/// Workspace index on the target monitor (zero-indexed)
|
||||
target_workspace: usize,
|
||||
}
|
||||
|
||||
macro_rules! gen_focused_workspace_padding_subcommand_args {
|
||||
// SubCommand Pattern
|
||||
( $( $name:ident ),+ $(,)? ) => {
|
||||
@@ -720,13 +729,25 @@ struct LoadCustomLayout {
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct Subscribe {
|
||||
struct SubscribeSocket {
|
||||
/// Name of the socket to send event notifications to
|
||||
socket: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct UnsubscribeSocket {
|
||||
/// Name of the socket to stop sending event notifications to
|
||||
socket: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct SubscribePipe {
|
||||
/// Name of the pipe to send event notifications to (without "\\.\pipe\" prepended)
|
||||
named_pipe: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, AhkFunction)]
|
||||
struct Unsubscribe {
|
||||
struct UnsubscribePipe {
|
||||
/// Name of the pipe to stop sending event notifications to (without "\\.\pipe\" prepended)
|
||||
named_pipe: String,
|
||||
}
|
||||
@@ -792,21 +813,37 @@ enum SubCommand {
|
||||
Start(Start),
|
||||
/// Stop the komorebi.exe process and restore all hidden windows
|
||||
Stop(Stop),
|
||||
/// Output various important komorebi-related environment values
|
||||
/// Check komorebi configuration and related files for common errors
|
||||
Check,
|
||||
/// Show the path to komorebi.json
|
||||
#[clap(alias = "config")]
|
||||
Configuration,
|
||||
/// Show the path to whkdrc
|
||||
#[clap(alias = "whkd")]
|
||||
Whkdrc,
|
||||
/// Show a JSON representation of the current window manager state
|
||||
State,
|
||||
/// Show a JSON representation of the current global state
|
||||
GlobalState,
|
||||
/// Show a JSON representation of visible windows
|
||||
VisibleWindows,
|
||||
/// Query the current window manager state
|
||||
#[clap(arg_required_else_help = true)]
|
||||
Query(Query),
|
||||
/// Subscribe to komorebi events
|
||||
/// Subscribe to komorebi events using a Unix Domain Socket
|
||||
#[clap(arg_required_else_help = true)]
|
||||
Subscribe(Subscribe),
|
||||
SubscribeSocket(SubscribeSocket),
|
||||
/// Unsubscribe from komorebi events
|
||||
#[clap(arg_required_else_help = true)]
|
||||
Unsubscribe(Unsubscribe),
|
||||
UnsubscribeSocket(UnsubscribeSocket),
|
||||
/// Subscribe to komorebi events using a Named Pipe
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "subscribe")]
|
||||
SubscribePipe(SubscribePipe),
|
||||
/// Unsubscribe from komorebi events
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "unsubscribe")]
|
||||
UnsubscribePipe(UnsubscribePipe),
|
||||
/// Tail komorebi.exe's process logs (cancel with Ctrl-C)
|
||||
Log,
|
||||
/// Quicksave the current resize layout dimensions
|
||||
@@ -889,6 +926,9 @@ enum SubCommand {
|
||||
/// Send the focused window to the specified monitor workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
SendToMonitorWorkspace(SendToMonitorWorkspace),
|
||||
/// Move the focused window to the specified monitor workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
MoveToMonitorWorkspace(MoveToMonitorWorkspace),
|
||||
/// Focus the specified monitor
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusMonitor(FocusMonitor),
|
||||
@@ -1050,8 +1090,9 @@ enum SubCommand {
|
||||
WatchConfiguration(WatchConfiguration),
|
||||
/// Signal that the final configuration option has been sent
|
||||
CompleteConfiguration,
|
||||
/// Enable or disable a hack simulating ALT key presses to ensure focus changes succeed
|
||||
/// DEPRECATED since v0.1.22
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(hide = true)]
|
||||
AltFocusHack(AltFocusHack),
|
||||
/// Set the window behaviour when switching workspaces / cycling stacks
|
||||
#[clap(arg_required_else_help = true)]
|
||||
@@ -1097,7 +1138,7 @@ enum SubCommand {
|
||||
/// Toggle title bars for whitelisted applications
|
||||
ToggleTitleBars,
|
||||
/// Identify an application that has overflowing borders
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(hide = true)]
|
||||
#[clap(alias = "identify-border-overflow")]
|
||||
IdentifyBorderOverflowApplication(IdentifyBorderOverflowApplication),
|
||||
/// Enable or disable the active window border
|
||||
@@ -1160,46 +1201,31 @@ enum SubCommand {
|
||||
pub fn send_message(bytes: &[u8]) -> Result<()> {
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(bytes)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(bytes)?;
|
||||
Ok(stream.shutdown(Shutdown::Write)?)
|
||||
}
|
||||
|
||||
fn with_komorebic_socket<F: Fn() -> Result<()>>(f: F) -> Result<()> {
|
||||
let socket = DATA_DIR.join("komorebic.sock");
|
||||
pub fn send_query(bytes: &[u8]) -> Result<String> {
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(()) => {}
|
||||
Err(error) => match error.kind() {
|
||||
// Doing this because ::exists() doesn't work reliably on Windows via IntelliJ
|
||||
ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
};
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(bytes)?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
|
||||
f()?;
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut response = String::new();
|
||||
reader.read_to_string(&mut response)?;
|
||||
|
||||
let listener = UnixListener::bind(socket)?;
|
||||
match listener.accept() {
|
||||
Ok(incoming) => {
|
||||
let stream = BufReader::new(incoming.0);
|
||||
for line in stream.lines() {
|
||||
println!("{}", line?);
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
panic!("{}", error);
|
||||
}
|
||||
// print_query is a helper that queries komorebi and prints the response.
|
||||
// panics on error.
|
||||
fn print_query(bytes: &[u8]) {
|
||||
match send_query(bytes) {
|
||||
Ok(response) => println!("{response}"),
|
||||
Err(error) => panic!("{}", error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1231,9 +1257,15 @@ fn main() -> Result<()> {
|
||||
let subcommands = cli.get_subcommands_mut();
|
||||
std::fs::create_dir_all("docs/cli")?;
|
||||
|
||||
let ignore = [
|
||||
"docgen",
|
||||
"alt-focus-hack",
|
||||
"identify-border-overflow-application",
|
||||
];
|
||||
|
||||
for cmd in subcommands {
|
||||
let name = cmd.get_name().to_string();
|
||||
if name != "docgen" {
|
||||
if !ignore.contains(&name.as_str()) {
|
||||
let help_text = cmd.render_long_help().to_string();
|
||||
let outpath = format!("docs/cli/{name}.md");
|
||||
let markdown = format!("# {name}\n\n```\n{help_text}\n```");
|
||||
@@ -1243,27 +1275,20 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
SubCommand::Quickstart => {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let home_dir = dirs::home_dir().expect("could not find home dir");
|
||||
let config_dir = home_dir.join(".config");
|
||||
let local_appdata_dir = data_local_dir().expect("could not find localdata dir");
|
||||
let data_dir = local_appdata_dir.join("komorebi");
|
||||
std::fs::create_dir_all(&config_dir)?;
|
||||
std::fs::create_dir_all(data_dir)?;
|
||||
|
||||
let komorebi_json = reqwest::blocking::get(
|
||||
format!("https://raw.githubusercontent.com/LGUG2Z/komorebi/v{version}/komorebi.example.json")
|
||||
)?.text()?;
|
||||
let komorebi_json = include_str!("../../docs/komorebi.example.json");
|
||||
std::fs::write(HOME_DIR.join("komorebi.json"), komorebi_json)?;
|
||||
|
||||
let applications_yaml = reqwest::blocking::get(
|
||||
"https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.yaml"
|
||||
)?
|
||||
.text()?;
|
||||
let applications_yaml = include_str!("../applications.yaml");
|
||||
std::fs::write(HOME_DIR.join("applications.yaml"), applications_yaml)?;
|
||||
|
||||
let whkdrc = reqwest::blocking::get(format!(
|
||||
"https://raw.githubusercontent.com/LGUG2Z/komorebi/v{version}/whkdrc.sample"
|
||||
))?
|
||||
.text()?;
|
||||
let whkdrc = include_str!("../../docs/whkdrc.sample");
|
||||
std::fs::write(config_dir.join("whkdrc"), whkdrc)?;
|
||||
|
||||
println!("Example ~/komorebi.json, ~/.config/whkdrc and latest ~/applications.yaml files downloaded");
|
||||
@@ -1351,7 +1376,7 @@ fn main() -> Result<()> {
|
||||
let diagnostic = ConfigurationError {
|
||||
message: msgs[0].to_string(),
|
||||
src: NamedSource::new("komorebi.json", config_source.clone()),
|
||||
bad_bit: SourceSpan::new(offset, 2.into()),
|
||||
bad_bit: SourceSpan::new(offset, 2),
|
||||
};
|
||||
|
||||
println!("{:?}", Report::new(diagnostic));
|
||||
@@ -1383,6 +1408,11 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Check that this file adheres to the schema static config schema as the last step,
|
||||
// so that more basic errors above can be shown to the error before schema-specific
|
||||
// errors
|
||||
let _ = serde_json::from_str::<komorebi_client::StaticConfig>(&config_source)?;
|
||||
|
||||
if config_whkd.exists() {
|
||||
println!("Found {}; key bindings will be loaded from here when whkd is started, and you can start it automatically using the --whkd flag\n", config_whkd.to_string_lossy());
|
||||
} else {
|
||||
@@ -1405,6 +1435,20 @@ fn main() -> Result<()> {
|
||||
println!("If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration\n");
|
||||
}
|
||||
}
|
||||
SubCommand::Configuration => {
|
||||
let static_config = HOME_DIR.join("komorebi.json");
|
||||
|
||||
if static_config.exists() {
|
||||
println!("{}", static_config.display());
|
||||
}
|
||||
}
|
||||
SubCommand::Whkdrc => {
|
||||
let whkdrc = WHKD_CONFIG_DIR.join("whkdrc");
|
||||
|
||||
if whkdrc.exists() {
|
||||
println!("{}", whkdrc.display());
|
||||
}
|
||||
}
|
||||
SubCommand::AhkLibrary => {
|
||||
let library = HOME_DIR.join("komorebic.lib.ahk");
|
||||
let mut file = OpenOptions::new()
|
||||
@@ -1438,7 +1482,7 @@ fn main() -> Result<()> {
|
||||
let color_log = std::env::temp_dir().join("komorebi.log");
|
||||
let file = TailedFile::new(File::open(color_log)?);
|
||||
let locked = file.lock();
|
||||
#[allow(clippy::significant_drop_in_scrutinee)]
|
||||
#[allow(clippy::significant_drop_in_scrutinee, clippy::lines_filter_map_ok)]
|
||||
for line in locked.lines().flatten() {
|
||||
println!("{line}");
|
||||
}
|
||||
@@ -1523,6 +1567,15 @@ fn main() -> Result<()> {
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::MoveToMonitorWorkspace(arg) => {
|
||||
send_message(
|
||||
&SocketMessage::MoveContainerToMonitorWorkspaceNumber(
|
||||
arg.target_monitor,
|
||||
arg.target_workspace,
|
||||
)
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::MoveWorkspaceToMonitor(arg) => {
|
||||
send_message(&SocketMessage::MoveWorkspaceToMonitorNumber(arg.target).as_bytes()?)?;
|
||||
}
|
||||
@@ -1735,7 +1788,7 @@ fn main() -> Result<()> {
|
||||
let exec = if let Ok(output) = Command::new("where.exe").arg("komorebi.ps1").output() {
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
match stdout.trim() {
|
||||
stdout if stdout.is_empty() => None,
|
||||
"" => None,
|
||||
// It's possible that a komorebi.ps1 config will be in %USERPROFILE% - ignore this
|
||||
stdout if !stdout.contains("scoop") => None,
|
||||
stdout => {
|
||||
@@ -1753,7 +1806,7 @@ fn main() -> Result<()> {
|
||||
};
|
||||
|
||||
let mut flags = vec![];
|
||||
if let Some(config) = arg.config {
|
||||
if let Some(config) = &arg.config {
|
||||
let path = resolve_home_path(config)?;
|
||||
if !path.is_file() {
|
||||
bail!("could not find file: {}", path.display());
|
||||
@@ -1788,9 +1841,10 @@ fn main() -> Result<()> {
|
||||
)
|
||||
};
|
||||
|
||||
let mut attempts = 0;
|
||||
let mut running = false;
|
||||
|
||||
while !running {
|
||||
while !running && attempts <= 2 {
|
||||
match powershell_script::run(&script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
@@ -1811,9 +1865,27 @@ fn main() -> Result<()> {
|
||||
running = true;
|
||||
} else {
|
||||
println!("komorebi.exe did not start... Trying again");
|
||||
attempts += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if !running {
|
||||
println!("\nRunning komorebi.exe directly for detailed error output\n");
|
||||
if let Some(config) = arg.config {
|
||||
let path = resolve_home_path(config)?;
|
||||
if let Ok(output) = Command::new("komorebi.exe")
|
||||
.arg(format!("'--config=\"{}\"'", path.display()))
|
||||
.output()
|
||||
{
|
||||
println!("{}", String::from_utf8(output.stderr)?);
|
||||
}
|
||||
} else if let Ok(output) = Command::new("komorebi.exe").output() {
|
||||
println!("{}", String::from_utf8(output.stderr)?);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if arg.whkd {
|
||||
let script = r"
|
||||
if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
@@ -1851,6 +1923,13 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nThank you for using komorebi!\n");
|
||||
println!("* Become a sponsor https://github.com/sponsors/LGUG2Z - Even $1/month makes a big difference");
|
||||
println!(
|
||||
"* Subscribe to https://youtube.com/@LGUG2Z - Live dev videos and feature previews"
|
||||
);
|
||||
println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, share your desktops");
|
||||
}
|
||||
SubCommand::Stop(arg) => {
|
||||
if arg.whkd {
|
||||
@@ -1868,6 +1947,34 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
send_message(&SocketMessage::Stop.as_bytes()?)?;
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes();
|
||||
|
||||
if system.processes_by_name("komorebi.exe").count() >= 1 {
|
||||
println!("komorebi is still running, attempting to force-quit");
|
||||
|
||||
let script = r"
|
||||
Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
|
||||
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
|
||||
|
||||
let file = File::open(hwnd_json)?;
|
||||
let reader = BufReader::new(file);
|
||||
let hwnds: Vec<isize> = serde_json::from_reader(reader)?;
|
||||
|
||||
for hwnd in hwnds {
|
||||
restore_window(HWND(hwnd));
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SubCommand::FloatRule(arg) => {
|
||||
send_message(&SocketMessage::FloatRule(arg.identifier, arg.id).as_bytes()?)?;
|
||||
@@ -1996,15 +2103,16 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
)?;
|
||||
}
|
||||
SubCommand::State => {
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::State.as_bytes()?))?;
|
||||
print_query(&SocketMessage::State.as_bytes()?);
|
||||
}
|
||||
SubCommand::GlobalState => {
|
||||
print_query(&SocketMessage::GlobalState.as_bytes()?);
|
||||
}
|
||||
SubCommand::VisibleWindows => {
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::VisibleWindows.as_bytes()?))?;
|
||||
print_query(&SocketMessage::VisibleWindows.as_bytes()?);
|
||||
}
|
||||
SubCommand::Query(arg) => {
|
||||
with_komorebic_socket(|| {
|
||||
send_message(&SocketMessage::Query(arg.state_query).as_bytes()?)
|
||||
})?;
|
||||
print_query(&SocketMessage::Query(arg.state_query).as_bytes()?);
|
||||
}
|
||||
SubCommand::RestoreWindows => {
|
||||
let hwnd_json = DATA_DIR.join("komorebi.hwnd.json");
|
||||
@@ -2038,9 +2146,6 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
SubCommand::CompleteConfiguration => {
|
||||
send_message(&SocketMessage::CompleteConfiguration.as_bytes()?)?;
|
||||
}
|
||||
SubCommand::AltFocusHack(arg) => {
|
||||
send_message(&SocketMessage::AltFocusHack(arg.boolean_state.into()).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::IdentifyObjectNameChangeApplication(target) => {
|
||||
send_message(
|
||||
&SocketMessage::IdentifyObjectNameChangeApplication(target.identifier, target.id)
|
||||
@@ -2058,12 +2163,6 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::IdentifyBorderOverflowApplication(target) => {
|
||||
send_message(
|
||||
&SocketMessage::IdentifyBorderOverflowApplication(target.identifier, target.id)
|
||||
.as_bytes()?,
|
||||
)?;
|
||||
}
|
||||
SubCommand::RemoveTitleBar(target) => {
|
||||
match target.identifier {
|
||||
ApplicationIdentifier::Exe => {}
|
||||
@@ -2095,11 +2194,17 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
SubCommand::LoadResize(arg) => {
|
||||
send_message(&SocketMessage::Load(resolve_home_path(arg.path)?).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Subscribe(arg) => {
|
||||
send_message(&SocketMessage::AddSubscriber(arg.named_pipe).as_bytes()?)?;
|
||||
SubCommand::SubscribeSocket(arg) => {
|
||||
send_message(&SocketMessage::AddSubscriberSocket(arg.socket).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::Unsubscribe(arg) => {
|
||||
send_message(&SocketMessage::RemoveSubscriber(arg.named_pipe).as_bytes()?)?;
|
||||
SubCommand::UnsubscribeSocket(arg) => {
|
||||
send_message(&SocketMessage::RemoveSubscriberSocket(arg.socket).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::SubscribePipe(arg) => {
|
||||
send_message(&SocketMessage::AddSubscriberPipe(arg.named_pipe).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::UnsubscribePipe(arg) => {
|
||||
send_message(&SocketMessage::RemoveSubscriberPipe(arg.named_pipe).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ToggleMouseFollowsFocus => {
|
||||
send_message(&SocketMessage::ToggleMouseFollowsFocus.as_bytes()?)?;
|
||||
@@ -2117,10 +2222,10 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
)?;
|
||||
}
|
||||
SubCommand::ActiveWindowBorderWidth(arg) => {
|
||||
send_message(&SocketMessage::ActiveWindowBorderWidth(arg.width).as_bytes()?)?;
|
||||
send_message(&SocketMessage::BorderWidth(arg.width).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ActiveWindowBorderOffset(arg) => {
|
||||
send_message(&SocketMessage::ActiveWindowBorderOffset(arg.offset).as_bytes()?)?;
|
||||
send_message(&SocketMessage::BorderOffset(arg.offset).as_bytes()?)?;
|
||||
}
|
||||
SubCommand::ResizeDelta(arg) => {
|
||||
send_message(&SocketMessage::ResizeDelta(arg.pixels).as_bytes()?)?;
|
||||
@@ -2235,23 +2340,23 @@ Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
);
|
||||
}
|
||||
SubCommand::ApplicationSpecificConfigurationSchema => {
|
||||
with_komorebic_socket(|| {
|
||||
send_message(&SocketMessage::ApplicationSpecificConfigurationSchema.as_bytes()?)
|
||||
})?;
|
||||
print_query(&SocketMessage::ApplicationSpecificConfigurationSchema.as_bytes()?);
|
||||
}
|
||||
SubCommand::NotificationSchema => {
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::NotificationSchema.as_bytes()?))?;
|
||||
print_query(&SocketMessage::NotificationSchema.as_bytes()?);
|
||||
}
|
||||
SubCommand::SocketSchema => {
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::SocketSchema.as_bytes()?))?;
|
||||
print_query(&SocketMessage::SocketSchema.as_bytes()?);
|
||||
}
|
||||
SubCommand::StaticConfigSchema => {
|
||||
with_komorebic_socket(|| send_message(&SocketMessage::StaticConfigSchema.as_bytes()?))?;
|
||||
print_query(&SocketMessage::StaticConfigSchema.as_bytes()?);
|
||||
}
|
||||
SubCommand::GenerateStaticConfig => {
|
||||
with_komorebic_socket(|| {
|
||||
send_message(&SocketMessage::GenerateStaticConfig.as_bytes()?)
|
||||
})?;
|
||||
print_query(&SocketMessage::GenerateStaticConfig.as_bytes()?);
|
||||
}
|
||||
// Deprecated
|
||||
SubCommand::AltFocusHack(_) | SubCommand::IdentifyBorderOverflowApplication(_) => {
|
||||
println!("Command deprecated - this is now automatically handled by komorebi! 🎉");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
308
mkdocs.yml
308
mkdocs.yml
@@ -39,161 +39,173 @@ theme:
|
||||
- search.share
|
||||
- search.suggest
|
||||
- toc.follow
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- pymdownx.highlight
|
||||
- pymdownx.superfences
|
||||
plugins:
|
||||
- macros
|
||||
- search
|
||||
|
||||
nav:
|
||||
- Komorebi:
|
||||
- index.md
|
||||
- Design: design.md
|
||||
- index.md
|
||||
- Design: design.md
|
||||
- Getting started:
|
||||
- Installation: installation.md
|
||||
- Example configurations: example-configurations.md
|
||||
- Installation: installation.md
|
||||
- Example configurations: example-configurations.md
|
||||
- Troubleshooting: troubleshooting.md
|
||||
- Common workflows:
|
||||
- common-workflows/komorebi-config-home.md
|
||||
- common-workflows/active-window-border.md
|
||||
- common-workflows/remove-gaps.md
|
||||
- common-workflows/ignore-windows.md
|
||||
- common-workflows/force-manage-windows.md
|
||||
- common-workflows/tray-and-multi-window-applications.md
|
||||
- common-workflows/focus-follows-mouse.md
|
||||
- common-workflows/mouse-follows-focus.md
|
||||
- common-workflows/custom-layouts.md
|
||||
- common-workflows/dynamic-layout-switching.md
|
||||
# - common-workflows/autohotkey.md
|
||||
- common-workflows/komorebi-config-home.md
|
||||
- common-workflows/active-window-border.md
|
||||
- common-workflows/stackbar.md
|
||||
- common-workflows/remove-gaps.md
|
||||
- common-workflows/ignore-windows.md
|
||||
- common-workflows/force-manage-windows.md
|
||||
- common-workflows/tray-and-multi-window-applications.md
|
||||
- common-workflows/focus-follows-mouse.md
|
||||
- common-workflows/mouse-follows-focus.md
|
||||
- common-workflows/custom-layouts.md
|
||||
- common-workflows/dynamic-layout-switching.md
|
||||
# - common-workflows/autohotkey.md
|
||||
- Release notes:
|
||||
- release/v0-1-22.md
|
||||
- Configuration reference: https://komorebi.lgug2z.com/schema
|
||||
- CLI reference:
|
||||
- cli/quickstart.md
|
||||
- cli/start.md
|
||||
- cli/stop.md
|
||||
- cli/check.md
|
||||
- cli/state.md
|
||||
- cli/visible-windows.md
|
||||
- cli/query.md
|
||||
- cli/subscribe.md
|
||||
- cli/unsubscribe.md
|
||||
- cli/log.md
|
||||
- cli/quick-save-resize.md
|
||||
- cli/quick-load-resize.md
|
||||
- cli/save-resize.md
|
||||
- cli/load-resize.md
|
||||
- cli/focus.md
|
||||
- cli/move.md
|
||||
- cli/minimize.md
|
||||
- cli/close.md
|
||||
- cli/force-focus.md
|
||||
- cli/cycle-focus.md
|
||||
- cli/cycle-move.md
|
||||
- cli/stack.md
|
||||
- cli/resize-edge.md
|
||||
- cli/resize-axis.md
|
||||
- cli/unstack.md
|
||||
- cli/cycle-stack.md
|
||||
- cli/move-to-monitor.md
|
||||
- cli/cycle-move-to-monitor.md
|
||||
- cli/move-to-workspace.md
|
||||
- cli/move-to-named-workspace.md
|
||||
- cli/cycle-move-to-workspace.md
|
||||
- cli/send-to-monitor.md
|
||||
- cli/cycle-send-to-monitor.md
|
||||
- cli/send-to-workspace.md
|
||||
- cli/send-to-named-workspace.md
|
||||
- cli/cycle-send-to-workspace.md
|
||||
- cli/send-to-monitor-workspace.md
|
||||
- cli/focus-monitor.md
|
||||
- cli/focus-last-workspace.md
|
||||
- cli/focus-workspace.md
|
||||
- cli/focus-workspaces.md
|
||||
- cli/focus-monitor-workspace.md
|
||||
- cli/focus-named-workspace.md
|
||||
- cli/cycle-monitor.md
|
||||
- cli/cycle-workspace.md
|
||||
- cli/move-workspace-to-monitor.md
|
||||
- cli/swap-workspaces-with-monitor.md
|
||||
- cli/new-workspace.md
|
||||
- cli/resize-delta.md
|
||||
- cli/invisible-borders.md
|
||||
- cli/global-work-area-offset.md
|
||||
- cli/monitor-work-area-offset.md
|
||||
- cli/focused-workspace-container-padding.md
|
||||
- cli/focused-workspace-padding.md
|
||||
- cli/adjust-container-padding.md
|
||||
- cli/adjust-workspace-padding.md
|
||||
- cli/change-layout.md
|
||||
- cli/cycle-layout.md
|
||||
- cli/load-custom-layout.md
|
||||
- cli/flip-layout.md
|
||||
- cli/promote.md
|
||||
- cli/promote-focus.md
|
||||
- cli/retile.md
|
||||
- cli/monitor-index-preference.md
|
||||
- cli/display-index-preference.md
|
||||
- cli/ensure-workspaces.md
|
||||
- cli/ensure-named-workspaces.md
|
||||
- cli/container-padding.md
|
||||
- cli/named-workspace-container-padding.md
|
||||
- cli/workspace-padding.md
|
||||
- cli/named-workspace-padding.md
|
||||
- cli/workspace-layout.md
|
||||
- cli/named-workspace-layout.md
|
||||
- cli/workspace-custom-layout.md
|
||||
- cli/named-workspace-custom-layout.md
|
||||
- cli/workspace-layout-rule.md
|
||||
- cli/named-workspace-layout-rule.md
|
||||
- cli/workspace-custom-layout-rule.md
|
||||
- cli/named-workspace-custom-layout-rule.md
|
||||
- cli/clear-workspace-layout-rules.md
|
||||
- cli/clear-named-workspace-layout-rules.md
|
||||
- cli/workspace-tiling.md
|
||||
- cli/named-workspace-tiling.md
|
||||
- cli/workspace-name.md
|
||||
- cli/toggle-window-container-behaviour.md
|
||||
- cli/toggle-pause.md
|
||||
- cli/toggle-tiling.md
|
||||
- cli/toggle-float.md
|
||||
- cli/toggle-monocle.md
|
||||
- cli/toggle-maximize.md
|
||||
- cli/restore-windows.md
|
||||
- cli/manage.md
|
||||
- cli/unmanage.md
|
||||
- cli/reload-configuration.md
|
||||
- cli/watch-configuration.md
|
||||
- cli/complete-configuration.md
|
||||
- cli/alt-focus-hack.md
|
||||
- cli/window-hiding-behaviour.md
|
||||
- cli/cross-monitor-move-behaviour.md
|
||||
- cli/toggle-cross-monitor-move-behaviour.md
|
||||
- cli/unmanaged-window-operation-behaviour.md
|
||||
- cli/float-rule.md
|
||||
- cli/manage-rule.md
|
||||
- cli/initial-workspace-rule.md
|
||||
- cli/initial-named-workspace-rule.md
|
||||
- cli/workspace-rule.md
|
||||
- cli/named-workspace-rule.md
|
||||
- cli/identify-object-name-change-application.md
|
||||
- cli/identify-tray-application.md
|
||||
- cli/identify-layered-application.md
|
||||
- cli/remove-title-bar.md
|
||||
- cli/toggle-title-bars.md
|
||||
- cli/identify-border-overflow-application.md
|
||||
- cli/active-window-border.md
|
||||
- cli/active-window-border-colour.md
|
||||
- cli/active-window-border-width.md
|
||||
- cli/active-window-border-offset.md
|
||||
- cli/focus-follows-mouse.md
|
||||
- cli/toggle-focus-follows-mouse.md
|
||||
- cli/mouse-follows-focus.md
|
||||
- cli/toggle-mouse-follows-focus.md
|
||||
- cli/ahk-library.md
|
||||
- cli/ahk-app-specific-configuration.md
|
||||
- cli/pwsh-app-specific-configuration.md
|
||||
- cli/format-app-specific-configuration.md
|
||||
- cli/fetch-app-specific-configuration.md
|
||||
- cli/application-specific-configuration-schema.md
|
||||
- cli/notification-schema.md
|
||||
- cli/socket-schema.md
|
||||
- cli/static-config-schema.md
|
||||
- cli/generate-static-config.md
|
||||
- cli/enable-autostart.md
|
||||
- cli/disable-autostart.md
|
||||
- cli/quickstart.md
|
||||
- cli/start.md
|
||||
- cli/stop.md
|
||||
- cli/check.md
|
||||
- cli/configuration.md
|
||||
- cli/whkdrc.md
|
||||
- cli/state.md
|
||||
- cli/global-state.md
|
||||
- cli/visible-windows.md
|
||||
- cli/query.md
|
||||
- cli/subscribe-socket.md
|
||||
- cli/unsubscribe-socket.md
|
||||
- cli/subscribe-pipe.md
|
||||
- cli/unsubscribe-pipe.md
|
||||
- cli/log.md
|
||||
- cli/quick-save-resize.md
|
||||
- cli/quick-load-resize.md
|
||||
- cli/save-resize.md
|
||||
- cli/load-resize.md
|
||||
- cli/focus.md
|
||||
- cli/move.md
|
||||
- cli/minimize.md
|
||||
- cli/close.md
|
||||
- cli/force-focus.md
|
||||
- cli/cycle-focus.md
|
||||
- cli/cycle-move.md
|
||||
- cli/stack.md
|
||||
- cli/resize-edge.md
|
||||
- cli/resize-axis.md
|
||||
- cli/unstack.md
|
||||
- cli/cycle-stack.md
|
||||
- cli/move-to-monitor.md
|
||||
- cli/cycle-move-to-monitor.md
|
||||
- cli/move-to-workspace.md
|
||||
- cli/move-to-named-workspace.md
|
||||
- cli/cycle-move-to-workspace.md
|
||||
- cli/send-to-monitor.md
|
||||
- cli/cycle-send-to-monitor.md
|
||||
- cli/send-to-workspace.md
|
||||
- cli/send-to-named-workspace.md
|
||||
- cli/cycle-send-to-workspace.md
|
||||
- cli/send-to-monitor-workspace.md
|
||||
- cli/move-to-monitor-workspace.md
|
||||
- cli/focus-monitor.md
|
||||
- cli/focus-last-workspace.md
|
||||
- cli/focus-workspace.md
|
||||
- cli/focus-workspaces.md
|
||||
- cli/focus-monitor-workspace.md
|
||||
- cli/focus-named-workspace.md
|
||||
- cli/cycle-monitor.md
|
||||
- cli/cycle-workspace.md
|
||||
- cli/move-workspace-to-monitor.md
|
||||
- cli/swap-workspaces-with-monitor.md
|
||||
- cli/new-workspace.md
|
||||
- cli/resize-delta.md
|
||||
- cli/invisible-borders.md
|
||||
- cli/global-work-area-offset.md
|
||||
- cli/monitor-work-area-offset.md
|
||||
- cli/focused-workspace-container-padding.md
|
||||
- cli/focused-workspace-padding.md
|
||||
- cli/adjust-container-padding.md
|
||||
- cli/adjust-workspace-padding.md
|
||||
- cli/change-layout.md
|
||||
- cli/cycle-layout.md
|
||||
- cli/load-custom-layout.md
|
||||
- cli/flip-layout.md
|
||||
- cli/promote.md
|
||||
- cli/promote-focus.md
|
||||
- cli/retile.md
|
||||
- cli/monitor-index-preference.md
|
||||
- cli/display-index-preference.md
|
||||
- cli/ensure-workspaces.md
|
||||
- cli/ensure-named-workspaces.md
|
||||
- cli/container-padding.md
|
||||
- cli/named-workspace-container-padding.md
|
||||
- cli/workspace-padding.md
|
||||
- cli/named-workspace-padding.md
|
||||
- cli/workspace-layout.md
|
||||
- cli/named-workspace-layout.md
|
||||
- cli/workspace-custom-layout.md
|
||||
- cli/named-workspace-custom-layout.md
|
||||
- cli/workspace-layout-rule.md
|
||||
- cli/named-workspace-layout-rule.md
|
||||
- cli/workspace-custom-layout-rule.md
|
||||
- cli/named-workspace-custom-layout-rule.md
|
||||
- cli/clear-workspace-layout-rules.md
|
||||
- cli/clear-named-workspace-layout-rules.md
|
||||
- cli/workspace-tiling.md
|
||||
- cli/named-workspace-tiling.md
|
||||
- cli/workspace-name.md
|
||||
- cli/toggle-window-container-behaviour.md
|
||||
- cli/toggle-pause.md
|
||||
- cli/toggle-tiling.md
|
||||
- cli/toggle-float.md
|
||||
- cli/toggle-monocle.md
|
||||
- cli/toggle-maximize.md
|
||||
- cli/restore-windows.md
|
||||
- cli/manage.md
|
||||
- cli/unmanage.md
|
||||
- cli/reload-configuration.md
|
||||
- cli/watch-configuration.md
|
||||
- cli/complete-configuration.md
|
||||
- cli/window-hiding-behaviour.md
|
||||
- cli/cross-monitor-move-behaviour.md
|
||||
- cli/toggle-cross-monitor-move-behaviour.md
|
||||
- cli/unmanaged-window-operation-behaviour.md
|
||||
- cli/float-rule.md
|
||||
- cli/manage-rule.md
|
||||
- cli/initial-workspace-rule.md
|
||||
- cli/initial-named-workspace-rule.md
|
||||
- cli/workspace-rule.md
|
||||
- cli/named-workspace-rule.md
|
||||
- cli/identify-object-name-change-application.md
|
||||
- cli/identify-tray-application.md
|
||||
- cli/identify-layered-application.md
|
||||
- cli/remove-title-bar.md
|
||||
- cli/toggle-title-bars.md
|
||||
- cli/active-window-border.md
|
||||
- cli/active-window-border-colour.md
|
||||
- cli/active-window-border-width.md
|
||||
- cli/active-window-border-offset.md
|
||||
- cli/focus-follows-mouse.md
|
||||
- cli/toggle-focus-follows-mouse.md
|
||||
- cli/mouse-follows-focus.md
|
||||
- cli/toggle-mouse-follows-focus.md
|
||||
- cli/ahk-library.md
|
||||
- cli/ahk-app-specific-configuration.md
|
||||
- cli/pwsh-app-specific-configuration.md
|
||||
- cli/format-app-specific-configuration.md
|
||||
- cli/fetch-app-specific-configuration.md
|
||||
- cli/application-specific-configuration-schema.md
|
||||
- cli/notification-schema.md
|
||||
- cli/socket-schema.md
|
||||
- cli/static-config-schema.md
|
||||
- cli/generate-static-config.md
|
||||
- cli/enable-autostart.md
|
||||
- cli/disable-autostart.md
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title"
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"ApplicationOptions": {
|
||||
|
||||
1002
schema.json
1002
schema.json
File diff suppressed because it is too large
Load Diff
11
shell.nix
Normal file
11
shell.nix
Normal file
@@ -0,0 +1,11 @@
|
||||
{pkgs ? import <nixpkgs> {}}:
|
||||
with pkgs;
|
||||
mkShell {
|
||||
name = "komorebi";
|
||||
|
||||
buildInputs = [
|
||||
python311Packages.mkdocs-material
|
||||
python311Packages.mkdocs-macros
|
||||
python311Packages.setuptools
|
||||
];
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
.shell powershell
|
||||
|
||||
# Reload whkd configuration
|
||||
# alt + o : taskkill /f /im whkd.exe && start /b whkd # if shell is cmd
|
||||
alt + o : taskkill /f /im whkd.exe; Start-Process whkd -WindowStyle hidden # if shell is pwsh / powershell
|
||||
alt + shift + o : komorebic reload-configuration
|
||||
|
||||
# App shortcuts - these require shell to be pwsh / powershell
|
||||
# The apps will be focused if open, or launched if not open
|
||||
# alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox }
|
||||
# alt + b : if ($wshell.AppActivate('Chrome') -eq $False) { start chrome }
|
||||
|
||||
# Focus windows
|
||||
alt + h : komorebic focus left
|
||||
alt + j : komorebic focus down
|
||||
alt + k : komorebic focus up
|
||||
alt + l : komorebic focus right
|
||||
alt + shift + oem_4 : komorebic cycle-focus previous # oem_4 is [
|
||||
alt + shift + oem_6 : komorebic cycle-focus next # oem_6 is ]
|
||||
|
||||
# Move windows
|
||||
alt + shift + h : komorebic move left
|
||||
alt + shift + j : komorebic move down
|
||||
alt + shift + k : komorebic move up
|
||||
alt + shift + l : komorebic move right
|
||||
alt + shift + return : komorebic promote
|
||||
|
||||
# Stack windows
|
||||
alt + left : komorebic stack left
|
||||
alt + down : komorebic stack down
|
||||
alt + up : komorebic stack up
|
||||
alt + right : komorebic stack right
|
||||
alt + oem_1 : komorebic unstack # oem_1 is ;
|
||||
alt + oem_4 : komorebic cycle-stack previous # oem_4 is [
|
||||
alt + oem_6 : komorebic cycle-stack next # oem_6 is ]
|
||||
|
||||
# Resize
|
||||
alt + oem_plus : komorebic resize-axis horizontal increase
|
||||
alt + oem_minus : komorebic resize-axis horizontal decrease
|
||||
alt + shift + oem_plus : komorebic resize-axis vertical increase
|
||||
alt + shift + oem_minus : komorebic resize-axis vertical decrease
|
||||
|
||||
# Manipulate windows
|
||||
alt + t : komorebic toggle-float
|
||||
alt + shift + f : komorebic toggle-monocle
|
||||
|
||||
# Window manager options
|
||||
alt + shift + r : komorebic retile
|
||||
alt + p : komorebic toggle-pause
|
||||
|
||||
# Layouts
|
||||
alt + x : komorebic flip-layout horizontal
|
||||
alt + y : komorebic flip-layout vertical
|
||||
|
||||
# Workspaces
|
||||
alt + 1 : komorebic focus-workspace 0
|
||||
alt + 2 : komorebic focus-workspace 1
|
||||
alt + 3 : komorebic focus-workspace 2
|
||||
|
||||
# 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
|
||||
Reference in New Issue
Block a user