mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-14 17:27:40 +01:00
Compare commits
113 Commits
v0.1.27
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62047ed1ab | ||
|
|
7f42042322 | ||
|
|
36b9beb294 | ||
|
|
73ea3c51ef | ||
|
|
6db8f440dd | ||
|
|
0b7d5a89b7 | ||
|
|
a03a483185 | ||
|
|
6b28d76446 | ||
|
|
2325d750c5 | ||
|
|
24da24f177 | ||
|
|
dc6e326e69 | ||
|
|
8752bbbaf1 | ||
|
|
30e09d9946 | ||
|
|
8d5e40eb16 | ||
|
|
98a2aa4b23 | ||
|
|
400f90105a | ||
|
|
c6e76d2906 | ||
|
|
46e6d89770 | ||
|
|
2f0a93058f | ||
|
|
75d5971cc8 | ||
|
|
861d415551 | ||
|
|
20f370a51d | ||
|
|
2d1613b4d9 | ||
|
|
216154b975 | ||
|
|
fe9b7e53b7 | ||
|
|
d9bffa06df | ||
|
|
d34363af8f | ||
|
|
db4b0dd63b | ||
|
|
7b563aac5e | ||
|
|
b7198242ff | ||
|
|
a1f1be0afe | ||
|
|
e4b7adeb0f | ||
|
|
818ac3404c | ||
|
|
d6ae81af13 | ||
|
|
109227b74c | ||
|
|
ddb600f745 | ||
|
|
167ec92811 | ||
|
|
d110e12a62 | ||
|
|
21bd09e419 | ||
|
|
01ccf70ad5 | ||
|
|
c3f135703e | ||
|
|
3720ce42d0 | ||
|
|
1080159e68 | ||
|
|
360d0915a1 | ||
|
|
182c1e6a96 | ||
|
|
14d2ebd756 | ||
|
|
50b89cc1df | ||
|
|
df409902bb | ||
|
|
df19d06333 | ||
|
|
96d094d9d7 | ||
|
|
2916256e79 | ||
|
|
21a2138330 | ||
|
|
6addfed1ce | ||
|
|
6ba0ba79f9 | ||
|
|
254fcc988f | ||
|
|
7005a01d9c | ||
|
|
de3d4d0d99 | ||
|
|
b69db863f1 | ||
|
|
286bb0070c | ||
|
|
45894be4ff | ||
|
|
bc67936dd3 | ||
|
|
c06d9afa3b | ||
|
|
b799fd3077 | ||
|
|
3c03528750 | ||
|
|
821a124771 | ||
|
|
8f7b9202b2 | ||
|
|
13e2cbc7a1 | ||
|
|
ff653e78af | ||
|
|
6d038b8b18 | ||
|
|
45a5941872 | ||
|
|
f54097f094 | ||
|
|
29b14f8dc8 | ||
|
|
a1cf5ba29c | ||
|
|
a60e5a77c2 | ||
|
|
f722905be1 | ||
|
|
b5eafc6b96 | ||
|
|
c367967301 | ||
|
|
780635c8ef | ||
|
|
d5bec7afa4 | ||
|
|
974aa0d000 | ||
|
|
0f9c23b6f4 | ||
|
|
6ea71834a1 | ||
|
|
81451cb17a | ||
|
|
7653495e31 | ||
|
|
0cdce8fc2a | ||
|
|
67f14730d0 | ||
|
|
ef9e734680 | ||
|
|
faa7786979 | ||
|
|
3c8a6cb7bd | ||
|
|
50a279239a | ||
|
|
2c8f25ef82 | ||
|
|
bdc1cad597 | ||
|
|
e2f2d6b919 | ||
|
|
aebe8792ac | ||
|
|
9fb6f8ebcd | ||
|
|
6eb6129618 | ||
|
|
5b997b6ea6 | ||
|
|
9414466646 | ||
|
|
9e87baa8b8 | ||
|
|
2a67c9c786 | ||
|
|
60bc96e9a5 | ||
|
|
5e9d573f0b | ||
|
|
128db85054 | ||
|
|
cc7dbde049 | ||
|
|
a5735c4186 | ||
|
|
48cb3db2fe | ||
|
|
24bff7e527 | ||
|
|
b985659e30 | ||
|
|
1d0ac9b555 | ||
|
|
7b1ece9680 | ||
|
|
00fc5382f6 | ||
|
|
9e37baa88a | ||
|
|
9a65a4ae92 |
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,52 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]: Short descriptive title"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See bug
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots and Videos**
|
||||
Add screenshots and videos to help explain your problem.
|
||||
|
||||
**Operating System**
|
||||
Provide the output of `systeminfo | grep "^OS Name\|^OS Version"`
|
||||
|
||||
For example:
|
||||
```
|
||||
OS Name: Microsoft Windows 11 Pro
|
||||
OS Version: 10.0.22000 N/A Build 22000
|
||||
```
|
||||
|
||||
**`komorebic check` Output**
|
||||
Provide the output of `komorebic check`
|
||||
|
||||
For example:
|
||||
```
|
||||
No KOMOREBI_CONFIG_HOME detected, defaulting to C:\Users\LGUG2Z
|
||||
|
||||
Looking for configuration files in C:\Users\LGUG2Z
|
||||
|
||||
No komorebi configuration found in C:\Users\LGUG2Z
|
||||
|
||||
If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration
|
||||
```
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
In particular, if you have any other AHK scripts or software running that handle any aspect of window management or manipulation
|
||||
55
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
55
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
labels: [ bug ]
|
||||
body:
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
description: >
|
||||
Please provide a short summary of the bug, along with any information
|
||||
you feel is relevant to replicating the bug.
|
||||
|
||||
You may include screenshots and videos in this section.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Version Information
|
||||
description: >
|
||||
Please provide information about the versions of Windows and komorebi
|
||||
running on your machine.
|
||||
|
||||
Do not submit a bug if you are not using an official version of Windows
|
||||
such as AtlasOS; only official versions of Windows are supported.
|
||||
|
||||
```
|
||||
systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version"
|
||||
```
|
||||
|
||||
```
|
||||
komorebic --version
|
||||
```
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Komorebi Configuration
|
||||
description: >
|
||||
Please provide your configuration file (komorebi.json or komorebi.bar.json)
|
||||
render: json
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Hotkey Configuration
|
||||
description: >
|
||||
Please provide your whkdrc or komorebi.ahk hotkey configuration file
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Output of komorebic check
|
||||
description: >
|
||||
Please provide the output of `komorebic check`
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Komorebi Documentation
|
||||
url: https://lgug2z.github.io/komorebi/
|
||||
about: Please search the documentation website before opening an issue
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEAT]: Short descriptive title"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
18
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
18
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Feature request
|
||||
description: Suggest an improvement
|
||||
labels: [ enhancement ]
|
||||
body:
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Suggestion
|
||||
description: >
|
||||
Please share your suggestion here. Be sure to include all necessary context.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: >
|
||||
Please share share alternatives you have considered here.
|
||||
31
.github/workflows/windows.yaml
vendored
31
.github/workflows/windows.yaml
vendored
@@ -14,14 +14,16 @@ on:
|
||||
tags:
|
||||
- v*
|
||||
schedule:
|
||||
- cron: "30 0 * * 1" # Every Monday at half past midnight UTC
|
||||
- cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: windows-latest
|
||||
permissions: write-all
|
||||
env:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -93,24 +95,47 @@ jobs:
|
||||
target/${{ matrix.target }}/release/komorebi.exe
|
||||
target/${{ matrix.target }}/release/komorebic.exe
|
||||
target/${{ matrix.target }}/release/komorebic-no-console.exe
|
||||
target/${{ matrix.target }}/release/komorebi-bar.exe
|
||||
target/${{ matrix.target }}/release/komorebi-gui.exe
|
||||
target/${{ matrix.target }}/release/komorebi.pdb
|
||||
target/${{ matrix.target }}/release/komorebic.pdb
|
||||
target/${{ matrix.target }}/release/komorebi-gui.pdb
|
||||
target/${{ matrix.target }}/release/komorebi_gui.pdb
|
||||
target/wix/komorebi-*.msi
|
||||
retention-days: 7
|
||||
- name: Check GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
env:
|
||||
GORELEASER_CURRENT_TAG: v0.1.29
|
||||
with:
|
||||
version: latest
|
||||
args: build --skip=validate --clean
|
||||
|
||||
- name: Prepare nightly artifacts
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
|
||||
run: |
|
||||
Compress-Archive .\target\${{ matrix.target }}\release\*.exe komorebi-nightly-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./target/wix/*.msi -Destination ./komorebi-nightly-x86_64.msi
|
||||
echo "$((Get-FileHash komorebi-nightly-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-nightly-x86_64-pc-windows-msvc.zip" >checksums.txt
|
||||
- name: Update nightly
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
|
||||
shell: bash
|
||||
run: |
|
||||
gh release delete nightly --yes || true
|
||||
git push origin :nightly || true
|
||||
gh release create nightly \
|
||||
--target $GITHUB_SHA \
|
||||
--prerelease \
|
||||
--title "komorebi nightly (${GITHUB_SHA})" \
|
||||
--notes "This nightly release of komorebi corresponds to [this commit](https://github.com/LGUG2Z/komorebi/commit/${GITHUB_SHA})." \
|
||||
komorebi-nightly-x86_64-pc-windows-msvc.zip \
|
||||
komorebi-nightly-x86_64.msi \
|
||||
checksums.txt
|
||||
# Release
|
||||
- name: Generate changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: |
|
||||
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
|
||||
git tag -d nightly
|
||||
kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md"
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
||||
CHANGELOG.md
|
||||
dummy.go
|
||||
komorebic/applications.yaml
|
||||
/.vs
|
||||
|
||||
@@ -44,6 +44,15 @@ builds:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-gui.exe" ".\dist\komorebi-gui_windows_amd64_v1\komorebi-gui.exe"
|
||||
- id: komorebi-bar
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebi-bar
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-bar.exe" ".\dist\komorebi-bar_windows_amd64_v1\komorebi-bar.exe"
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
|
||||
|
||||
3165
Cargo.lock
generated
3165
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
38
Cargo.toml
38
Cargo.toml
@@ -4,26 +4,44 @@ resolver = "2"
|
||||
members = [
|
||||
"komorebi",
|
||||
"komorebi-client",
|
||||
"komorebi-core",
|
||||
"komorebi-gui",
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes"
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = { version = "4", features = ["derive", "wrap_help"] }
|
||||
chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.29"
|
||||
egui_extras = "0.29"
|
||||
dirs = "5"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
schemars = "0.8"
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
||||
sysinfo = "0.30"
|
||||
serde_yaml = "0.9"
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
paste = "1"
|
||||
sysinfo = "0.31"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "2a0f7166da154880a1750b91829b1186d9c6a00c" }
|
||||
windows-implement = { version = "0.53" }
|
||||
windows-interface = { version = "0.53" }
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
|
||||
windows-implement = { version = "0.58" }
|
||||
windows-interface = { version = "0.58" }
|
||||
windows-core = { version = "0.58" }
|
||||
shadow-rs = "0.35"
|
||||
which = "6"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.54"
|
||||
version = "0.58"
|
||||
features = [
|
||||
"implement",
|
||||
"Win32_System_Com",
|
||||
@@ -40,5 +58,11 @@ features = [
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_SystemServices"
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control"
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
27
LICENSE.md
27
LICENSE.md
@@ -1,6 +1,6 @@
|
||||
# PolyForm Strict License 1.0.0
|
||||
# Komorebi License
|
||||
|
||||
<https://polyformproject.org/licenses/strict/1.0.0>
|
||||
Version 1.0.0
|
||||
|
||||
## Acceptance
|
||||
|
||||
@@ -13,8 +13,14 @@ your licenses.
|
||||
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.
|
||||
permitted purpose. However, you may only make changes according
|
||||
to the [Changes License](#changes-license), and you may not
|
||||
distribute the software or new works based on the software.
|
||||
|
||||
## Changes License
|
||||
|
||||
The licensor grants you an additional copyright license to
|
||||
make changes for any permitted purpose.
|
||||
|
||||
## Patent License
|
||||
|
||||
@@ -22,10 +28,6 @@ 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
|
||||
@@ -34,15 +36,6 @@ 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
|
||||
|
||||
8
PRIVACY.md
Normal file
8
PRIVACY.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Privacy Policy for Komorebi
|
||||
|
||||
No data about your device(s) or _komorebi_ usage leave your device.
|
||||
|
||||
## Data Maintained by Komorebi
|
||||
|
||||
_komorebi_ writes log files to and keeps a list of temporary window handles (HWNDs) currently managed by the process in
|
||||
the `$Env:LOCALAPPDATA\komorebi\` directory. This directory is owned by the user running the process.
|
||||
20
README.md
20
README.md
@@ -60,7 +60,7 @@ channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
|
||||
_komorebi_ development videos. If you would like to be notified of upcoming
|
||||
videos please subscribe and turn on notifications.
|
||||
|
||||
_komorebi_ is a free and open-source project, and one that encourages you to
|
||||
_komorebi_ is a free and source-available project, and one that encourages you to
|
||||
make charitable donations if you find the software to be useful and have the
|
||||
financial means.
|
||||
|
||||
@@ -82,7 +82,7 @@ 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)
|
||||
[](https://www.youtube.com/watch?v=MMZUAtHbTYY)
|
||||
|
||||
# Comparison With Fancy Zones
|
||||
|
||||
@@ -99,6 +99,13 @@ video will answer the majority of your questions.
|
||||
|
||||
# Demonstrations
|
||||
|
||||
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
|
||||
unfocused window transparency and animations enabled, using a custom status bar integrated using
|
||||
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
|
||||
|
||||
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
|
||||
|
||||
|
||||
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
|
||||
11 with a terminal emulator, a web browser and a code editor. The original
|
||||
video can be viewed
|
||||
@@ -184,7 +191,8 @@ required.
|
||||
|
||||
## License
|
||||
|
||||
`komorebi` is licensed under the [PolyForm Strict 1.0.0
|
||||
`komorebi` is licensed under the [Komorebi 1.0.0 license](./LICENSE.md), which
|
||||
is a fork of 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
|
||||
@@ -272,7 +280,7 @@ If the named pipe exists, `komorebi` will start pushing JSON data of successfull
|
||||
|
||||
You may then filter on the `type` key to listen to the events that you are interested in. For a full list of possible
|
||||
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
|
||||
in `komorebi-core`.
|
||||
in `komorebi::core`.
|
||||
|
||||
Below is an example of how you can subscribe to and filter on events using a named pipe in `nodejs`.
|
||||
|
||||
@@ -351,7 +359,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.25"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.29"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
@@ -407,7 +415,7 @@ A TCP listener can optionally be exposed on a port of your choosing with the `--
|
||||
provided to `komorebi` or `komorebic start`, no TCP listener will be created.
|
||||
|
||||
Once created, your client may send
|
||||
any [SocketMessage](https://github.com/LGUG2Z/komorebi/blob/master/komorebi-core/src/lib.rs#L37) to `komorebi` in the
|
||||
any [SocketMessage](https://github.com/LGUG2Z/komorebi/blob/master/komorebi/src/core/mod.rs#L37) to `komorebi` in the
|
||||
same way that `komorebic` would.
|
||||
|
||||
This can be used if you would like to create your own alternative to `komorebic` which incorporates scripting and
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# ahk-library
|
||||
|
||||
```
|
||||
Generate a library of AutoHotKey helper functions
|
||||
|
||||
Usage: komorebic.exe komorebic.exe ahk-library
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/animation-duration.md
Normal file
16
docs/cli/animation-duration.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# animation-duration
|
||||
|
||||
```
|
||||
Set the duration for movement animations in ms
|
||||
|
||||
Usage: komorebic.exe animation-duration <DURATION>
|
||||
|
||||
Arguments:
|
||||
<DURATION>
|
||||
Desired animation durations in ms
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/animation-fps.md
Normal file
16
docs/cli/animation-fps.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# animation-fps
|
||||
|
||||
```
|
||||
Set the frames per second for movement animations
|
||||
|
||||
Usage: komorebic.exe animation-fps <FPS>
|
||||
|
||||
Arguments:
|
||||
<FPS>
|
||||
Desired animation frames per second
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
20
docs/cli/animation-style.md
Normal file
20
docs/cli/animation-style.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# animation-style
|
||||
|
||||
```
|
||||
Set the ease function for movement animations
|
||||
|
||||
Usage: komorebic.exe animation-style [OPTIONS]
|
||||
|
||||
Options:
|
||||
-s, --style <STYLE>
|
||||
Desired ease function for animation
|
||||
|
||||
[default: linear]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart,
|
||||
ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ,
|
||||
ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/animation.md
Normal file
16
docs/cli/animation.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# animation
|
||||
|
||||
```
|
||||
Enable or disable movement animations
|
||||
|
||||
Usage: komorebic.exe animation <BOOLEAN_STATE>
|
||||
|
||||
Arguments:
|
||||
<BOOLEAN_STATE>
|
||||
[possible values: enable, disable]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/bar-configuration.md
Normal file
12
docs/cli/bar-configuration.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# bar-configuration
|
||||
|
||||
```
|
||||
Show the path to komorebi.bar.json
|
||||
|
||||
Usage: komorebic.exe bar-configuration
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
20
docs/cli/border-implementation.md
Normal file
20
docs/cli/border-implementation.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# border-implementation
|
||||
|
||||
```
|
||||
Set the border implementation
|
||||
|
||||
Usage: komorebic.exe border-implementation <STYLE>
|
||||
|
||||
Arguments:
|
||||
<STYLE>
|
||||
Desired border implementation
|
||||
|
||||
Possible values:
|
||||
- komorebi: Use the adjustable komorebi border implementation
|
||||
- windows: Use the thin Windows accent border implementation
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
```
|
||||
21
docs/cli/border-style.md
Normal file
21
docs/cli/border-style.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# border-style
|
||||
|
||||
```
|
||||
Set the border style
|
||||
|
||||
Usage: komorebic.exe border-style <STYLE>
|
||||
|
||||
Arguments:
|
||||
<STYLE>
|
||||
Desired border style
|
||||
|
||||
Possible values:
|
||||
- system: Use the system border style
|
||||
- rounded: Use the Windows 11-style rounded borders
|
||||
- square: Use the Windows 10-style square borders
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
```
|
||||
12
docs/cli/clear-all-workspace-rules.md
Normal file
12
docs/cli/clear-all-workspace-rules.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# clear-all-workspace-rules
|
||||
|
||||
```
|
||||
Remove all application association rules for all workspaces
|
||||
|
||||
Usage: komorebic.exe clear-all-workspace-rules
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/clear-named-workspace-rules.md
Normal file
16
docs/cli/clear-named-workspace-rules.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# clear-named-workspace-rules
|
||||
|
||||
```
|
||||
Remove all application association rules for a named workspace
|
||||
|
||||
Usage: komorebic.exe clear-named-workspace-rules <WORKSPACE>
|
||||
|
||||
Arguments:
|
||||
<WORKSPACE>
|
||||
Name of a workspace
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
19
docs/cli/clear-workspace-rules.md
Normal file
19
docs/cli/clear-workspace-rules.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# clear-workspace-rules
|
||||
|
||||
```
|
||||
Remove all application association rules for a workspace by monitor and workspace index
|
||||
|
||||
Usage: komorebic.exe clear-workspace-rules <MONITOR> <WORKSPACE>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# complete-configuration
|
||||
|
||||
```
|
||||
Signal that the final configuration option has been sent
|
||||
For legacy komorebi.ahk or komorebi.ps1 configurations, signal that the final configuration option has been sent
|
||||
|
||||
Usage: komorebic.exe complete-configuration
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ Options:
|
||||
--ahk
|
||||
Enable autostart of ahk
|
||||
|
||||
--bar
|
||||
Enable autostart of komorebi-bar
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
16
docs/cli/focus-stack-window.md
Normal file
16
docs/cli/focus-stack-window.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# focus-stack-window
|
||||
|
||||
```
|
||||
Focus the specified window index in the focused stack
|
||||
|
||||
Usage: komorebic.exe focus-stack-window <TARGET>
|
||||
|
||||
Arguments:
|
||||
<TARGET>
|
||||
Target index (zero-indexed)
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# reload-configuration
|
||||
|
||||
```
|
||||
Reload ~/komorebi.ahk (if it exists)
|
||||
Reload legacy komorebi.ahk or komorebi.ps1 configurations (if they exist)
|
||||
|
||||
Usage: komorebic.exe reload-configuration
|
||||
|
||||
|
||||
16
docs/cli/replace-configuration.md
Normal file
16
docs/cli/replace-configuration.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# replace-configuration
|
||||
|
||||
```
|
||||
Replace the configuration of a running instance of komorebi from a static configuration file
|
||||
|
||||
Usage: komorebic.exe replace-configuration <PATH>
|
||||
|
||||
Arguments:
|
||||
<PATH>
|
||||
Static configuration JSON file from which the configuration should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -24,6 +24,9 @@ Options:
|
||||
--ahk
|
||||
Start autohotkey configuration file
|
||||
|
||||
--bar
|
||||
Start komorebi-bar in a background process
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ Options:
|
||||
--whkd
|
||||
Stop whkd if it is running as a background process
|
||||
|
||||
--bar
|
||||
Stop komorebi-bar if it is running as a background process
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
12
docs/cli/toggle-transparency.md
Normal file
12
docs/cli/toggle-transparency.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-transparency
|
||||
|
||||
```
|
||||
Toggle transparency for unfocused windows
|
||||
|
||||
Usage: komorebic.exe toggle-transparency
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# watch-configuration
|
||||
|
||||
```
|
||||
Enable or disable watching of ~/komorebi.ahk (if it exists)
|
||||
Enable or disable watching of legacy komorebi.ahk or komorebi.ps1 configurations (if they exist)
|
||||
|
||||
Usage: komorebic.exe watch-configuration <BOOLEAN_STATE>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Arguments:
|
||||
Possible values:
|
||||
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
|
||||
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
25
docs/common-workflows/animations.md
Normal file
25
docs/common-workflows/animations.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Animations
|
||||
|
||||
If you would like to add window movement animations, ensure the following options are
|
||||
defined in the `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"animation": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Window movement animations only apply to actions taking place within the same monitor
|
||||
workspace.
|
||||
|
||||
You can optionally set a custom duration in ms with `animation.duration` (default: `250`),
|
||||
a custom style with `animation.style` (default: `Linear`), and a custom FPS value with
|
||||
`animation.fps` (default: `60`).
|
||||
|
||||
It is important to note that higher `fps` and a longer `duration` settings will result
|
||||
in increased CPU usage.
|
||||
|
||||
This feature is not considered stable, and you may encounter visual artifacts
|
||||
from time to time.
|
||||
@@ -16,16 +16,17 @@ 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.
|
||||
With the example configurations downloaded, you can now start `komorebi`,
|
||||
`komorebi-bar` and `whkd`.
|
||||
|
||||
```powershell
|
||||
komorebic start --whkd
|
||||
komorebic start --whkd --bar
|
||||
```
|
||||
|
||||
## komorebi.json
|
||||
|
||||
The example window manager configuration sets some sane defaults and provides
|
||||
five preconfigured workspaces on the primary monitor each with a different
|
||||
seven preconfigured workspaces on the primary monitor each with a different
|
||||
layout.
|
||||
|
||||
```json
|
||||
@@ -213,3 +214,24 @@ reference.
|
||||
If you want to use one of those key codes, put them into lower case and remove
|
||||
the `VK_` prefix. For example, the keycode `VK_OEM_PLUS` becomes `oem_plus` in
|
||||
the sample configuration above.
|
||||
|
||||
## komorebi.bar.json
|
||||
|
||||
The example status bar configuration sets some sane defaults and provides
|
||||
a number of pre-configured widgets on the primary monitor.
|
||||
|
||||
```json
|
||||
{% include "./komorebi.bar.example.json" %}
|
||||
```
|
||||
|
||||
### Themes
|
||||
|
||||
Themes can be set in either `komorebi.json` or `komorebi.bar.json`. If set
|
||||
in `komorebi.json`, the theme will be applied to both komorebi's borders and
|
||||
stackbars as well as the status bar.
|
||||
|
||||
If set in `komorebi.bar.json`, the theme will only be applied to the status bar.
|
||||
|
||||
All [Catppuccin palette variants](https://catppuccin.com/)
|
||||
and [most Base16 palette variants](https://tinted-theming.github.io/base16-gallery/)
|
||||
are available as themes.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Getting started
|
||||
|
||||
`komorebi` is a tiling window manager for Windows that is comprised of two
|
||||
`komorebi` is a tiling window manager for Windows that is comprised of two
|
||||
main binaries, `komorebi.exe`, which contains the window manager itself,
|
||||
and `komorebic.exe`, which is the main way to send commands to the tiling
|
||||
window manager.
|
||||
@@ -23,6 +23,10 @@ suggest that once you are familiar with the main `komorebic.exe` commands used
|
||||
to manipulate the window manager, you use
|
||||
[AutoHotKey](https://www.autohotkey.com/) to handle your key bindings.
|
||||
|
||||
`komorebi` also includes `komorebi-bar.exe`, a simple and reliable status bar which
|
||||
is deeply integrated with the tiling window manager, and can be customized with
|
||||
various widgets and themes.
|
||||
|
||||
## Installation
|
||||
|
||||
`komorebi` is available pre-built to install via
|
||||
@@ -115,6 +119,7 @@ cargo +stable install --path komorebi --locked
|
||||
cargo +stable install --path komorebic --locked
|
||||
cargo +stable install --path komorebic-no-console --locked
|
||||
cargo +stable install --path komorebi-gui --locked
|
||||
cargo +stable install --path komorebi-bar --locked
|
||||
```
|
||||
|
||||
If the binaries have been built and added to your `$PATH` correctly, you should
|
||||
@@ -131,8 +136,8 @@ first-time set up and running komorebi require an internet connection).
|
||||
|
||||
## Uninstallation
|
||||
|
||||
Before uninstalling, first run `komorebic stop --whkd` to make sure that both
|
||||
the `komorebi` and `whkd` processes have been stopped.
|
||||
Before uninstalling, first run `komorebic stop --whkd --bar` to make sure that
|
||||
the `komorebi`, `komorebi-bar` and `whkd` processes have been stopped.
|
||||
|
||||
Then, depending on whether you installed with Scoop or WinGet, run `scoop
|
||||
uninstall komorebi whkd` or `winget uninstall LGUG2Z.komorebi LGUG2Z.whkd`.
|
||||
|
||||
76
docs/komorebi.bar.example.json
Normal file
76
docs/komorebi.bar.example.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.bar.json",
|
||||
"monitor": {
|
||||
"index": 0,
|
||||
"work_area_offset": {
|
||||
"left": 0,
|
||||
"top": 40,
|
||||
"right": 0,
|
||||
"bottom": 40
|
||||
}
|
||||
},
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
"palette": "Base16",
|
||||
"name": "Ashes",
|
||||
"accent": "Base0D"
|
||||
},
|
||||
"left_widgets": [
|
||||
{
|
||||
"Komorebi": {
|
||||
"workspaces": {
|
||||
"enable": true,
|
||||
"hide_empty_workspaces": false
|
||||
},
|
||||
"layout": {
|
||||
"enable": true
|
||||
},
|
||||
"focused_window": {
|
||||
"enable": true,
|
||||
"show_icon": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"right_widgets": [
|
||||
{
|
||||
"Media": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Storage": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Memory": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Network": {
|
||||
"enable": true,
|
||||
"show_total_data_transmitted": true,
|
||||
"show_network_activity": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Date": {
|
||||
"enable": true,
|
||||
"format": "DayDateMonthYear"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Time": {
|
||||
"enable": true,
|
||||
"format": "TwentyFourHour"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Battery": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.25/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
@@ -8,20 +8,17 @@
|
||||
"border": true,
|
||||
"border_width": 8,
|
||||
"border_offset": -1,
|
||||
"border_colours": {
|
||||
"single": "#42a5f5",
|
||||
"stack": "#00a542",
|
||||
"monocle": "#ff3399",
|
||||
"unfocused": "#808080"
|
||||
"theme": {
|
||||
"palette": "Base16",
|
||||
"name": "Ashes",
|
||||
"unfocused_border": "Base03",
|
||||
"bar_accent": "Base0D"
|
||||
},
|
||||
"stackbar": {
|
||||
"height": 40,
|
||||
"mode": "OnStack",
|
||||
"tabs": {
|
||||
"width": 300,
|
||||
"focused_text": "#00a542",
|
||||
"unfocused_text": "#b3b3b3",
|
||||
"background": "#141414"
|
||||
"width": 300
|
||||
}
|
||||
},
|
||||
"monitors": [
|
||||
|
||||
13
justfile
13
justfile
@@ -19,6 +19,8 @@ install-target target:
|
||||
install:
|
||||
just install-target komorebic
|
||||
just install-target komorebic-no-console
|
||||
just install-target komorebi-gui
|
||||
just install-target komorebi-bar
|
||||
just install-target komorebi
|
||||
|
||||
run:
|
||||
@@ -40,9 +42,16 @@ deadlock $RUST_LOG="trace":
|
||||
cargo +stable run --bin komorebi --locked --features deadlock_detection
|
||||
|
||||
docgen:
|
||||
komorebic docgen
|
||||
cargo run --package komorebic -- docgen
|
||||
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
|
||||
|
||||
schemagen:
|
||||
komorebic static-config-schema > schema.json
|
||||
cargo run --package komorebic -- static-config-schema > schema.json
|
||||
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
|
||||
cargo run --package komorebi-bar -- --schema > schema.bar.json
|
||||
generate-schema-doc .\schema.json --config template_name=js_offline --config minify=false .\static-config-docs\
|
||||
|
||||
generate-schema-doc .\schema.bar.json --config template_name=js_offline --config minify=false .\bar-config-docs\
|
||||
|
||||
rm -Force .\bar-config-docs\schema.html
|
||||
mv .\bar-config-docs\schema.bar.html .\bar-config-docs\schema.html
|
||||
|
||||
37
komorebi-bar/Cargo.toml
Normal file
37
komorebi-bar/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui-phosphor = "0.7"
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
netdev = "0.31"
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
random_word = { version = "0.4", features = ["en"] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
starship-battery = "0.10"
|
||||
sysinfo = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
458
komorebi-bar/src/bar.rs
Normal file
458
komorebi-bar/src/bar.rs
Normal file
@@ -0,0 +1,458 @@
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiNotificationState;
|
||||
use crate::process_hwnd;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widget::WidgetConfig;
|
||||
use crate::BAR_HEIGHT;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_LEFT;
|
||||
use crate::MONITOR_RIGHT;
|
||||
use crate::MONITOR_TOP;
|
||||
use crossbeam_channel::Receiver;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::CentralPanel;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontData;
|
||||
use eframe::egui::FontDefinitions;
|
||||
use eframe::egui::FontFamily;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Layout;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::TextStyle;
|
||||
use font_loader::system_fonts;
|
||||
use font_loader::system_fonts::FontPropertyBuilder;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use komorebi_themes::Base16Value;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::CatppuccinValue;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Komobar {
|
||||
pub config: Arc<KomobarConfig>,
|
||||
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub rx_gui: Receiver<komorebi_client::Notification>,
|
||||
pub rx_config: Receiver<KomobarConfig>,
|
||||
pub bg_color: Rc<RefCell<Color32>>,
|
||||
pub scale_factor: f32,
|
||||
}
|
||||
|
||||
pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Color32>>) {
|
||||
match theme {
|
||||
KomobarTheme::Catppuccin {
|
||||
name: catppuccin,
|
||||
accent: catppuccin_value,
|
||||
} => match catppuccin {
|
||||
Catppuccin::Frappe => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::FRAPPE.base);
|
||||
}
|
||||
Catppuccin::Latte => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::LATTE.base);
|
||||
}
|
||||
Catppuccin::Macchiato => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::MACCHIATO.base);
|
||||
}
|
||||
Catppuccin::Mocha => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::MOCHA.base);
|
||||
}
|
||||
},
|
||||
KomobarTheme::Base16 {
|
||||
name: base16,
|
||||
accent: base16_value,
|
||||
} => {
|
||||
ctx.set_style(base16.style());
|
||||
let base16_value = base16_value.unwrap_or_default();
|
||||
let accent = base16_value.color32(base16);
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
});
|
||||
|
||||
bg_color.replace(base16.background());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Komobar {
|
||||
pub fn apply_config(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
config: &KomobarConfig,
|
||||
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
) {
|
||||
MAX_LABEL_WIDTH.store(
|
||||
config.max_label_width.unwrap_or(400.0) as i32,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
if let Some(font_family) = &config.font_family {
|
||||
tracing::info!("attempting to add custom font family: {font_family}");
|
||||
Self::add_custom_font(ctx, font_family);
|
||||
}
|
||||
|
||||
let position = config.position.clone().unwrap_or(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
|
||||
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
|
||||
}),
|
||||
end: Some(Position {
|
||||
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
|
||||
y: BAR_HEIGHT,
|
||||
}),
|
||||
});
|
||||
|
||||
if let Some(hwnd) = process_hwnd() {
|
||||
let start = position.start.unwrap_or(Position {
|
||||
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
|
||||
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
|
||||
});
|
||||
|
||||
let end = position.end.unwrap_or(Position {
|
||||
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
|
||||
y: BAR_HEIGHT,
|
||||
});
|
||||
|
||||
let rect = komorebi_client::Rect {
|
||||
left: start.x as i32,
|
||||
top: start.y as i32,
|
||||
right: end.x as i32,
|
||||
bottom: end.y as i32,
|
||||
};
|
||||
|
||||
let window = komorebi_client::Window::from(hwnd);
|
||||
match window.set_position(&rect, false) {
|
||||
Ok(_) => {
|
||||
tracing::info!("updated bar position");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match config.theme {
|
||||
Some(theme) => {
|
||||
apply_theme(ctx, theme, self.bg_color.clone());
|
||||
}
|
||||
None => {
|
||||
let 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");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let config = home_dir.join("komorebi.json");
|
||||
match komorebi_client::StaticConfig::read(&config) {
|
||||
Ok(config) => {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(ctx, KomobarTheme::from(theme), self.bg_color.clone());
|
||||
|
||||
let stack_accent = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name, stack_border, ..
|
||||
} => stack_border
|
||||
.unwrap_or(CatppuccinValue::Green)
|
||||
.color32(name.as_theme()),
|
||||
KomorebiTheme::Base16 {
|
||||
name, stack_border, ..
|
||||
} => stack_border.unwrap_or(Base16Value::Base0B).color32(name),
|
||||
};
|
||||
|
||||
if let Some(state) = &self.komorebi_notification_state {
|
||||
state.borrow_mut().stack_accent = Some(stack_accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
ctx.set_style(Style::default());
|
||||
self.bg_color.replace(Style::default().visuals.panel_fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(font_size) = &config.font_size {
|
||||
tracing::info!("attempting to set custom font size: {font_size}");
|
||||
Self::set_font_size(ctx, *font_size);
|
||||
}
|
||||
|
||||
let mut komorebi_widget = None;
|
||||
let mut komorebi_widget_idx = None;
|
||||
let mut komorebi_notification_state = previous_notification_state;
|
||||
let mut side = None;
|
||||
|
||||
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
|
||||
if let WidgetConfig::Komorebi(config) = widget_config {
|
||||
komorebi_widget = Some(Komorebi::from(config));
|
||||
komorebi_widget_idx = Some(idx);
|
||||
side = Some(Side::Left);
|
||||
}
|
||||
}
|
||||
|
||||
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
|
||||
if let WidgetConfig::Komorebi(config) = widget_config {
|
||||
komorebi_widget = Some(Komorebi::from(config));
|
||||
komorebi_widget_idx = Some(idx);
|
||||
side = Some(Side::Right);
|
||||
}
|
||||
}
|
||||
|
||||
let mut left_widgets = config
|
||||
.left_widgets
|
||||
.iter()
|
||||
.map(|config| config.as_boxed_bar_widget())
|
||||
.collect::<Vec<Box<dyn BarWidget>>>();
|
||||
|
||||
let mut right_widgets = config
|
||||
.right_widgets
|
||||
.iter()
|
||||
.map(|config| config.as_boxed_bar_widget())
|
||||
.collect::<Vec<Box<dyn BarWidget>>>();
|
||||
|
||||
if let (Some(idx), Some(mut widget), Some(side)) =
|
||||
(komorebi_widget_idx, komorebi_widget, side)
|
||||
{
|
||||
match komorebi_notification_state {
|
||||
None => {
|
||||
komorebi_notification_state = Some(widget.komorebi_notification_state.clone());
|
||||
}
|
||||
Some(ref previous) => {
|
||||
previous
|
||||
.borrow_mut()
|
||||
.update_from_config(&widget.komorebi_notification_state.borrow());
|
||||
|
||||
widget.komorebi_notification_state = previous.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let boxed: Box<dyn BarWidget> = Box::new(widget);
|
||||
match side {
|
||||
Side::Left => left_widgets[idx] = boxed,
|
||||
Side::Right => right_widgets[idx] = boxed,
|
||||
}
|
||||
}
|
||||
|
||||
right_widgets.reverse();
|
||||
|
||||
self.left_widgets = left_widgets;
|
||||
self.right_widgets = right_widgets;
|
||||
|
||||
tracing::info!("widget configuration options applied");
|
||||
|
||||
self.komorebi_notification_state = komorebi_notification_state;
|
||||
}
|
||||
pub fn new(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
rx_config: Receiver<KomobarConfig>,
|
||||
config: Arc<KomobarConfig>,
|
||||
) -> Self {
|
||||
let mut komobar = Self {
|
||||
config: config.clone(),
|
||||
komorebi_notification_state: None,
|
||||
left_widgets: vec![],
|
||||
right_widgets: vec![],
|
||||
rx_gui,
|
||||
rx_config,
|
||||
bg_color: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
|
||||
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
|
||||
};
|
||||
|
||||
komobar.apply_config(&cc.egui_ctx, &config, None);
|
||||
// needs a double apply the first time for some reason
|
||||
komobar.apply_config(&cc.egui_ctx, &config, None);
|
||||
|
||||
komobar
|
||||
}
|
||||
|
||||
fn set_font_size(ctx: &Context, font_size: f32) {
|
||||
ctx.style_mut(|style| {
|
||||
style.text_styles = [
|
||||
(TextStyle::Small, FontId::new(9.0, FontFamily::Proportional)),
|
||||
(
|
||||
TextStyle::Body,
|
||||
FontId::new(font_size, FontFamily::Proportional),
|
||||
),
|
||||
(
|
||||
TextStyle::Button,
|
||||
FontId::new(font_size, FontFamily::Proportional),
|
||||
),
|
||||
(
|
||||
TextStyle::Heading,
|
||||
FontId::new(18.0, FontFamily::Proportional),
|
||||
),
|
||||
(
|
||||
TextStyle::Monospace,
|
||||
FontId::new(font_size, FontFamily::Monospace),
|
||||
),
|
||||
]
|
||||
.into();
|
||||
});
|
||||
}
|
||||
|
||||
fn add_custom_font(ctx: &Context, name: &str) {
|
||||
let mut fonts = FontDefinitions::default();
|
||||
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
|
||||
|
||||
let property = FontPropertyBuilder::new().family(name).build();
|
||||
|
||||
if let Some((font, _)) = system_fonts::get(&property) {
|
||||
fonts
|
||||
.font_data
|
||||
.insert(name.to_owned(), FontData::from_owned(font));
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(FontFamily::Proportional)
|
||||
.or_default()
|
||||
.insert(0, name.to_owned());
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(FontFamily::Monospace)
|
||||
.or_default()
|
||||
.push(name.to_owned());
|
||||
|
||||
// Tell egui to use these fonts:
|
||||
ctx.set_fonts(fonts);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl eframe::App for Komobar {
|
||||
// TODO: I think this is needed for transparency??
|
||||
// fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
|
||||
// egui::Rgba::TRANSPARENT.to_array()
|
||||
// let mut background = Color32::from_gray(18).to_normalized_gamma_f32();
|
||||
// background[3] = 0.9;
|
||||
// background
|
||||
// }
|
||||
|
||||
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
|
||||
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
|
||||
self.apply_config(
|
||||
ctx,
|
||||
&self.config.clone(),
|
||||
self.komorebi_notification_state.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(updated_config) = self.rx_config.try_recv() {
|
||||
self.apply_config(
|
||||
ctx,
|
||||
&updated_config,
|
||||
self.komorebi_notification_state.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
|
||||
komorebi_notification_state
|
||||
.borrow_mut()
|
||||
.handle_notification(
|
||||
ctx,
|
||||
self.config.monitor.index,
|
||||
self.rx_gui.clone(),
|
||||
self.bg_color.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
let frame = if let Some(frame) = &self.config.frame {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::symmetric(
|
||||
frame.inner_margin.x,
|
||||
frame.inner_margin.y,
|
||||
))
|
||||
.fill(*self.bg_color.borrow())
|
||||
} else {
|
||||
Frame::none().fill(*self.bg_color.borrow())
|
||||
};
|
||||
|
||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
|
||||
for w in &mut self.left_widgets {
|
||||
w.render(ctx, ui);
|
||||
}
|
||||
});
|
||||
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
for w in &mut self.right_widgets {
|
||||
w.render(ctx, ui);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
160
komorebi-bar/src/battery.rs
Normal file
160
komorebi-bar/src/battery.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use starship_battery::units::ratio::percent;
|
||||
use starship_battery::Manager;
|
||||
use starship_battery::State;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct BatteryConfig {
|
||||
/// Enable the Battery widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<BatteryConfig> for Battery {
|
||||
fn from(value: BatteryConfig) -> Self {
|
||||
let manager = Manager::new().unwrap();
|
||||
let mut last_state = String::new();
|
||||
let mut state = None;
|
||||
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
|
||||
|
||||
if let Ok(mut batteries) = manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>();
|
||||
match first.state() {
|
||||
State::Charging => state = Some(BatteryState::Charging),
|
||||
State::Discharging => state = Some(BatteryState::Discharging),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
last_state = match prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
manager,
|
||||
last_state,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: prefix,
|
||||
state: state.unwrap_or(BatteryState::Discharging),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum BatteryState {
|
||||
Charging,
|
||||
Discharging,
|
||||
}
|
||||
|
||||
pub struct Battery {
|
||||
pub enable: bool,
|
||||
manager: Manager,
|
||||
pub state: BatteryState,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_state: String,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Battery {
|
||||
fn output(&mut self) -> String {
|
||||
let mut output = self.last_state.clone();
|
||||
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
output.clear();
|
||||
|
||||
if let Ok(mut batteries) = self.manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>();
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => self.state = BatteryState::Discharging,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
output = match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state.clone_from(&output);
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Battery {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let emoji = match self.state {
|
||||
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
|
||||
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
|
||||
};
|
||||
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
ui.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
178
komorebi-bar/src/config.rs
Normal file
178
komorebi-bar/src/config.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use crate::widget::WidgetConfig;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::Rect;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.30`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar positioning options
|
||||
#[serde(alias = "viewport")]
|
||||
pub position: Option<PositionConfig>,
|
||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)
|
||||
pub frame: Option<FrameConfig>,
|
||||
/// Monitor options
|
||||
pub monitor: MonitorConfig,
|
||||
/// Font family
|
||||
pub font_family: Option<String>,
|
||||
/// Font size (default: 12.5)
|
||||
pub font_size: Option<f32>,
|
||||
/// Max label width before text truncation (default: 400.0)
|
||||
pub max_label_width: Option<f32>,
|
||||
/// Theme
|
||||
pub theme: Option<KomobarTheme>,
|
||||
/// Left side widgets (ordered left-to-right)
|
||||
pub left_widgets: Vec<WidgetConfig>,
|
||||
/// Right side widgets (ordered left-to-right)
|
||||
pub right_widgets: Vec<WidgetConfig>,
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn aliases(raw: &str) {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("position", ["viewport"]);
|
||||
map.insert("end", ["inner_frame"]);
|
||||
|
||||
let mut display = false;
|
||||
|
||||
for aliases in map.values() {
|
||||
for a in aliases {
|
||||
if raw.contains(a) {
|
||||
display = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if display {
|
||||
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
|
||||
for (canonical, aliases) in map {
|
||||
for alias in aliases {
|
||||
if raw.contains(alias) {
|
||||
println!(r#""{alias}" is now "{canonical}""#);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PositionConfig {
|
||||
/// The desired starting position of the bar (0,0 = top left of the screen)
|
||||
#[serde(alias = "position")]
|
||||
pub start: Option<Position>,
|
||||
/// The desired size of the bar from the starting position (usually monitor width x desired height)
|
||||
#[serde(alias = "inner_size")]
|
||||
pub end: Option<Position>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FrameConfig {
|
||||
/// Margin inside the painted frame
|
||||
pub inner_margin: Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MonitorConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub index: usize,
|
||||
/// Automatically apply a work area offset for this monitor to accommodate the bar
|
||||
pub work_area_offset: Option<Rect>,
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let mut value: Self = match path.extension().unwrap().to_string_lossy().as_str() {
|
||||
"json" => serde_json::from_str(&content)?,
|
||||
_ => panic!("unsupported format"),
|
||||
};
|
||||
|
||||
if value.frame.is_none() {
|
||||
value.frame = Some(FrameConfig {
|
||||
inner_margin: Position { x: 10.0, y: 10.0 },
|
||||
});
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Position {
|
||||
/// X coordinate
|
||||
pub x: f32,
|
||||
/// Y coordinate
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl From<Position> for Vec2 {
|
||||
fn from(value: Position) -> Self {
|
||||
Self {
|
||||
x: value.x,
|
||||
y: value.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Position> for Pos2 {
|
||||
fn from(value: Position) -> Self {
|
||||
Self {
|
||||
x: value.x,
|
||||
y: value.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "palette")]
|
||||
pub enum KomobarTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
name: komorebi_themes::Catppuccin,
|
||||
accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
name: komorebi_themes::Base16,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<KomorebiTheme> for KomobarTheme {
|
||||
fn from(value: KomorebiTheme) -> Self {
|
||||
match value {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name, bar_accent, ..
|
||||
} => Self::Catppuccin {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
},
|
||||
KomorebiTheme::Base16 {
|
||||
name, bar_accent, ..
|
||||
} => Self::Base16 {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum LabelPrefix {
|
||||
/// Show no prefix
|
||||
None,
|
||||
/// Show an icon
|
||||
Icon,
|
||||
/// Show text
|
||||
Text,
|
||||
/// Show an icon and text
|
||||
IconAndText,
|
||||
}
|
||||
120
komorebi-bar/src/cpu.rs
Normal file
120
komorebi-bar/src/cpu.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::RefreshKind;
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CpuConfig {
|
||||
/// Enable the Cpu widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<CpuConfig> for Cpu {
|
||||
fn from(value: CpuConfig) -> Self {
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
|
||||
|
||||
system.refresh_cpu_usage();
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Cpu {
|
||||
pub enable: bool,
|
||||
system: System,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
fn output(&mut self) -> String {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.system.refresh_cpu_usage();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let used = self.system.global_cpu_usage();
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Cpu {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CPU.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
137
komorebi-bar/src/date.rs
Normal file
137
komorebi-bar/src/date.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DateConfig {
|
||||
/// Enable the Date widget
|
||||
pub enable: bool,
|
||||
/// Set the Date format
|
||||
pub format: DateFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<DateConfig> for Date {
|
||||
fn from(value: DateConfig) -> Self {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum DateFormat {
|
||||
/// Month/Date/Year format (09/08/24)
|
||||
MonthDateYear,
|
||||
/// Year-Month-Date format (2024-09-08)
|
||||
YearMonthDate,
|
||||
/// Date-Month-Year format (8-Sep-2024)
|
||||
DateMonthYear,
|
||||
/// Day Date Month Year format (8 September 2024)
|
||||
DayDateMonthYear,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl DateFormat {
|
||||
pub fn next(&mut self) {
|
||||
match self {
|
||||
DateFormat::MonthDateYear => *self = Self::YearMonthDate,
|
||||
DateFormat::YearMonthDate => *self = Self::DateMonthYear,
|
||||
DateFormat::DateMonthYear => *self = Self::DayDateMonthYear,
|
||||
DateFormat::DayDateMonthYear => *self = Self::MonthDateYear,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn fmt_string(&self) -> String {
|
||||
match self {
|
||||
DateFormat::MonthDateYear => String::from("%D"),
|
||||
DateFormat::YearMonthDate => String::from("%F"),
|
||||
DateFormat::DateMonthYear => String::from("%v"),
|
||||
DateFormat::DayDateMonthYear => String::from("%A %e %B %Y"),
|
||||
DateFormat::Custom(custom) => custom.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Date {
|
||||
pub enable: bool,
|
||||
pub format: DateFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
}
|
||||
|
||||
impl Date {
|
||||
fn output(&mut self) -> String {
|
||||
chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Date {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CALENDAR_DOTS.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.insert_str(0, "DATE: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.next()
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
543
komorebi-bar/src/komorebi.rs
Normal file
543
komorebi-bar/src/komorebi.rs
Normal file
@@ -0,0 +1,543 @@
|
||||
use crate::bar::apply_theme;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::WIDGET_SPACING;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ColorImage;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Image;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::SelectableLabel;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::TextureHandle;
|
||||
use eframe::egui::TextureOptions;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::CycleDirection;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiConfig {
|
||||
/// Configure the Workspaces widget
|
||||
pub workspaces: KomorebiWorkspacesConfig,
|
||||
/// Configure the Layout widget
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
/// Configure the Focused Window widget
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
/// Configure the Configuration Switcher widget
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiWorkspacesConfig {
|
||||
/// Enable the Komorebi Workspaces widget
|
||||
pub enable: bool,
|
||||
/// Hide workspaces without any windows
|
||||
pub hide_empty_workspaces: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiLayoutConfig {
|
||||
/// Enable the Komorebi Layout widget
|
||||
pub enable: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiFocusedWindowConfig {
|
||||
/// Enable the Komorebi Focused Window widget
|
||||
pub enable: bool,
|
||||
/// Show the icon of the currently focused window
|
||||
pub show_icon: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiConfigurationSwitcherConfig {
|
||||
/// Enable the Komorebi Configurations widget
|
||||
pub enable: bool,
|
||||
/// A map of display friendly name => path to configuration.json
|
||||
pub configurations: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl From<&KomorebiConfig> for Komorebi {
|
||||
fn from(value: &KomorebiConfig) -> Self {
|
||||
let configuration_switcher =
|
||||
if let Some(configuration_switcher) = &value.configuration_switcher {
|
||||
let mut configuration_switcher = configuration_switcher.clone();
|
||||
for (_, location) in configuration_switcher.configurations.iter_mut() {
|
||||
*location = dunce::simplified(&PathBuf::from(location.clone()))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
Some(configuration_switcher)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState {
|
||||
selected_workspace: String::new(),
|
||||
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
workspaces: vec![],
|
||||
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
|
||||
mouse_follows_focus: true,
|
||||
work_area_offset: None,
|
||||
focused_container_information: (vec![], vec![], 0),
|
||||
stack_accent: None,
|
||||
})),
|
||||
workspaces: value.workspaces,
|
||||
layout: value.layout,
|
||||
focused_window: value.focused_window,
|
||||
configuration_switcher,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Komorebi {
|
||||
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
|
||||
pub workspaces: KomorebiWorkspacesConfig,
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
|
||||
impl BarWidget for Komorebi {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
|
||||
|
||||
if self.workspaces.enable {
|
||||
let mut update = None;
|
||||
|
||||
for (i, ws) in komorebi_notification_state.workspaces.iter().enumerate() {
|
||||
if ui
|
||||
.add(SelectableLabel::new(
|
||||
komorebi_notification_state.selected_workspace.eq(ws),
|
||||
ws.to_string(),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
update = Some(ws.to_string());
|
||||
let mut proceed = true;
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: FocusWorkspaceNumber");
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed && komorebi_client::send_message(&SocketMessage::Retile).is_err() {
|
||||
tracing::error!("could not send message to komorebi: Retile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(update) = update {
|
||||
komorebi_notification_state.selected_workspace = update;
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if let Some(layout) = self.layout {
|
||||
if layout.enable {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(komorebi_notification_state.layout.to_string())
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
match komorebi_notification_state.layout {
|
||||
KomorebiLayout::Default(_) => {
|
||||
if komorebi_client::send_message(&SocketMessage::CycleLayout(
|
||||
CycleDirection::Next,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: CycleLayout");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Floating => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: ToggleTiling");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Paused => {
|
||||
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
|
||||
tracing::error!("could not send message to komorebi: TogglePause");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Custom => {}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(configuration_switcher) = &self.configuration_switcher {
|
||||
if configuration_switcher.enable {
|
||||
for (name, location) in configuration_switcher.configurations.iter() {
|
||||
let path = PathBuf::from(location);
|
||||
if path.is_file()
|
||||
&& ui
|
||||
.add(Label::new(name).selectable(false).sense(Sense::click()))
|
||||
.clicked()
|
||||
{
|
||||
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
|
||||
let mut proceed = true;
|
||||
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
|
||||
canonicalized,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: ReplaceConfiguration"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if let Some(rect) = komorebi_notification_state.work_area_offset {
|
||||
if proceed {
|
||||
match komorebi_client::send_query(&SocketMessage::Query(
|
||||
komorebi_client::StateQuery::FocusedMonitorIndex,
|
||||
)) {
|
||||
Ok(idx) => {
|
||||
if let Ok(monitor_idx) = idx.parse::<usize>() {
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MonitorWorkAreaOffset"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: Query"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(focused_window) = self.focused_window {
|
||||
if focused_window.enable {
|
||||
let titles = &komorebi_notification_state.focused_container_information.0;
|
||||
let icons = &komorebi_notification_state.focused_container_information.1;
|
||||
let focused_window_idx =
|
||||
komorebi_notification_state.focused_container_information.2;
|
||||
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
|
||||
for (i, (title, icon)) in iter.enumerate() {
|
||||
if focused_window.show_icon {
|
||||
if let Some(img) = icon {
|
||||
ui.add(
|
||||
Image::from(&img_to_texture(ctx, img))
|
||||
.maintain_aspect_ratio(true)
|
||||
.max_height(15.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if i == focused_window_idx {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let layout_job = LayoutJob::simple(
|
||||
title.to_string(),
|
||||
font_id.clone(),
|
||||
komorebi_notification_state
|
||||
.stack_accent
|
||||
.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
if titles.len() > 1 {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job).selectable(false).truncate(),
|
||||
);
|
||||
} else {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title).selectable(false).truncate(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
if custom_ui
|
||||
.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title)
|
||||
.selectable(false)
|
||||
.sense(Sense::click())
|
||||
.truncate(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
false,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(i))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusStackWindow"
|
||||
);
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
|
||||
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||
let pixels = rgba_image.as_flat_samples();
|
||||
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
|
||||
ctx.load_texture("icon", color_image, TextureOptions::default())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationState {
|
||||
pub workspaces: Vec<String>,
|
||||
pub selected_workspace: String,
|
||||
pub focused_container_information: (Vec<String>, Vec<Option<RgbaImage>>, usize),
|
||||
pub layout: KomorebiLayout,
|
||||
pub hide_empty_workspaces: bool,
|
||||
pub mouse_follows_focus: bool,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub stack_accent: Option<Color32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum KomorebiLayout {
|
||||
Default(komorebi_client::DefaultLayout),
|
||||
Floating,
|
||||
Paused,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl Display for KomorebiLayout {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
|
||||
KomorebiLayout::Floating => write!(f, "Floating"),
|
||||
KomorebiLayout::Paused => write!(f, "Paused"),
|
||||
KomorebiLayout::Custom => write!(f, "Custom"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KomorebiNotificationState {
|
||||
pub fn update_from_config(&mut self, config: &Self) {
|
||||
self.hide_empty_workspaces = config.hide_empty_workspaces;
|
||||
}
|
||||
|
||||
pub fn handle_notification(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
monitor_index: usize,
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
) {
|
||||
match rx_gui.try_recv() {
|
||||
Err(error) => match error {
|
||||
TryRecvError::Empty => {}
|
||||
TryRecvError::Disconnected => {
|
||||
tracing::error!(
|
||||
"failed to receive komorebi notification on gui thread: {error}"
|
||||
);
|
||||
}
|
||||
},
|
||||
Ok(notification) => {
|
||||
if let NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(path)) =
|
||||
notification.event
|
||||
{
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.mouse_follows_focus = notification.state.mouse_follows_focus;
|
||||
|
||||
let monitor = ¬ification.state.monitors.elements()[monitor_index];
|
||||
self.work_area_offset =
|
||||
notification.state.monitors.elements()[monitor_index].work_area_offset();
|
||||
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
|
||||
let mut workspaces = vec![];
|
||||
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
|
||||
|
||||
for (i, ws) in monitor.workspaces().iter().enumerate() {
|
||||
let should_add = if self.hide_empty_workspaces {
|
||||
focused_workspace_idx == i || !ws.containers().is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_add {
|
||||
workspaces
|
||||
.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
self.workspaces = workspaces;
|
||||
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
|
||||
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
|
||||
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
|
||||
};
|
||||
|
||||
if !*monitor.workspaces()[focused_workspace_idx].tile() {
|
||||
self.layout = KomorebiLayout::Floating;
|
||||
}
|
||||
|
||||
if notification.state.is_paused {
|
||||
self.layout = KomorebiLayout::Paused;
|
||||
}
|
||||
|
||||
if let Some(container) =
|
||||
monitor.workspaces()[focused_workspace_idx].monocle_container()
|
||||
{
|
||||
self.focused_container_information = (
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.title().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
container.focused_window_idx(),
|
||||
);
|
||||
} else if let Some(container) =
|
||||
monitor.workspaces()[focused_workspace_idx].focused_container()
|
||||
{
|
||||
self.focused_container_information = (
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.title().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
container.focused_window_idx(),
|
||||
);
|
||||
} else {
|
||||
self.focused_container_information.0.clear();
|
||||
self.focused_container_information.1.clear();
|
||||
self.focused_container_information.2 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
411
komorebi-bar/src/main.rs
Normal file
411
komorebi-bar/src/main.rs
Normal file
@@ -0,0 +1,411 @@
|
||||
mod bar;
|
||||
mod battery;
|
||||
mod config;
|
||||
mod cpu;
|
||||
mod date;
|
||||
mod komorebi;
|
||||
mod media;
|
||||
mod memory;
|
||||
mod network;
|
||||
mod storage;
|
||||
mod time;
|
||||
mod ui;
|
||||
mod widget;
|
||||
|
||||
use crate::bar::Komobar;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use clap::Parser;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::SubscribeOptions;
|
||||
use schemars::gen::SchemaSettings;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||
use windows::Win32::System::Threading::GetCurrentThreadId;
|
||||
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
|
||||
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
|
||||
pub static WIDGET_SPACING: f32 = 10.0;
|
||||
|
||||
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
|
||||
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
|
||||
pub static BAR_HEIGHT: f32 = 50.0;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
struct Opts {
|
||||
/// Print the JSON schema of the configuration file and exit
|
||||
#[clap(long)]
|
||||
schema: bool,
|
||||
/// Print a list of fonts available on this system and exit
|
||||
#[clap(long)]
|
||||
fonts: bool,
|
||||
/// Path to a JSON or YAML configuration file
|
||||
#[clap(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Write an example komorebi.bar.json to disk
|
||||
#[clap(long)]
|
||||
quickstart: bool,
|
||||
/// Print a list of aliases that can be renamed to canonical variants
|
||||
#[clap(long)]
|
||||
#[clap(hide = true)]
|
||||
aliases: bool,
|
||||
}
|
||||
|
||||
extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
unsafe {
|
||||
let mut process_id = 0;
|
||||
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
|
||||
|
||||
if process_id == GetCurrentProcessId() {
|
||||
*(lparam.0 as *mut HWND) = hwnd;
|
||||
BOOL::from(false) // Stop enumeration
|
||||
} else {
|
||||
BOOL::from(true) // Continue enumeration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_hwnd() -> Option<isize> {
|
||||
unsafe {
|
||||
let mut hwnd = HWND::default();
|
||||
let _ = EnumThreadWindows(
|
||||
GetCurrentThreadId(),
|
||||
Some(enum_window),
|
||||
LPARAM(&mut hwnd as *mut HWND as isize),
|
||||
);
|
||||
|
||||
if hwnd.0 as isize == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(hwnd.0 as isize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }?;
|
||||
|
||||
let opts: Opts = Opts::parse();
|
||||
|
||||
if opts.schema {
|
||||
let settings = SchemaSettings::default().with(|s| {
|
||||
s.option_nullable = false;
|
||||
s.option_add_null_type = false;
|
||||
s.inline_subschemas = true;
|
||||
});
|
||||
|
||||
let gen = settings.into_generator();
|
||||
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
|
||||
println!("{schema}");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
if opts.fonts {
|
||||
for font in system_fonts::query_all() {
|
||||
println!("{font}");
|
||||
}
|
||||
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::fmt::Subscriber::builder()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.finish(),
|
||||
)?;
|
||||
|
||||
let 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");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if opts.quickstart {
|
||||
let komorebi_bar_json = include_str!("../../docs/komorebi.bar.example.json").to_string();
|
||||
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
|
||||
println!(
|
||||
"Example komorebi.bar.json file written to {}",
|
||||
home_dir.as_path().display()
|
||||
);
|
||||
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let default_config_path = home_dir.join("komorebi.bar.json");
|
||||
|
||||
let config_path = opts.config.map_or_else(
|
||||
|| {
|
||||
if !default_config_path.is_file() {
|
||||
None
|
||||
} else {
|
||||
Some(default_config_path.clone())
|
||||
}
|
||||
},
|
||||
Option::from,
|
||||
);
|
||||
|
||||
let mut config = match config_path {
|
||||
None => {
|
||||
let komorebi_bar_json =
|
||||
include_str!("../../docs/komorebi.bar.example.json").to_string();
|
||||
|
||||
std::fs::write(&default_config_path, komorebi_bar_json)?;
|
||||
tracing::info!(
|
||||
"created example configuration file: {}",
|
||||
default_config_path.as_path().display()
|
||||
);
|
||||
|
||||
KomobarConfig::read(&default_config_path)?
|
||||
}
|
||||
Some(ref config) => {
|
||||
if !opts.aliases {
|
||||
tracing::info!(
|
||||
"found configuration file: {}",
|
||||
config.as_path().to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
KomobarConfig::read(config)?
|
||||
}
|
||||
};
|
||||
|
||||
let config_path = config_path.unwrap_or(default_config_path);
|
||||
|
||||
if opts.aliases {
|
||||
KomobarConfig::aliases(&std::fs::read_to_string(&config_path)?);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let state = serde_json::from_str::<komorebi_client::State>(&komorebi_client::send_query(
|
||||
&SocketMessage::State,
|
||||
)?)?;
|
||||
|
||||
MONITOR_RIGHT.store(
|
||||
state.monitors.elements()[config.monitor.index].size().right,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[config.monitor.index].size().top,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[config.monitor.index].size().left,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
match config.position {
|
||||
None => {
|
||||
config.position = Some(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().left as f32,
|
||||
y: state.monitors.elements()[config.monitor.index].size().top as f32,
|
||||
}),
|
||||
end: Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32,
|
||||
y: 50.0,
|
||||
}),
|
||||
})
|
||||
}
|
||||
Some(ref mut position) => {
|
||||
if position.start.is_none() {
|
||||
position.start = Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().left as f32,
|
||||
y: state.monitors.elements()[config.monitor.index].size().top as f32,
|
||||
});
|
||||
}
|
||||
|
||||
if position.end.is_none() {
|
||||
position.end = Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32,
|
||||
y: 50.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let viewport_builder = ViewportBuilder::default()
|
||||
.with_decorations(false)
|
||||
// .with_transparent(config.transparent)
|
||||
.with_taskbar(false);
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: viewport_builder,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(rect) = &config.monitor.work_area_offset {
|
||||
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
|
||||
config.monitor.index,
|
||||
*rect,
|
||||
))?;
|
||||
tracing::info!(
|
||||
"work area offset applied to monitor: {}",
|
||||
config.monitor.index
|
||||
);
|
||||
}
|
||||
|
||||
let (tx_gui, rx_gui) = crossbeam_channel::unbounded();
|
||||
let (tx_config, rx_config) = crossbeam_channel::unbounded();
|
||||
|
||||
let mut hotwatch = Hotwatch::new()?;
|
||||
let config_path_cl = config_path.clone();
|
||||
|
||||
hotwatch.watch(config_path, move |event| match event.kind {
|
||||
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
|
||||
Ok(updated) => {
|
||||
tracing::info!(
|
||||
"configuration file updated: {}",
|
||||
config_path_cl.as_path().to_string_lossy()
|
||||
);
|
||||
|
||||
if let Err(error) = tx_config.send(updated) {
|
||||
tracing::error!("could not send configuration update to gui: {error}")
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
})?;
|
||||
|
||||
tracing::info!("watching configuration file for changes");
|
||||
|
||||
let config_arc = Arc::new(config);
|
||||
eframe::run_native(
|
||||
"komorebi-bar",
|
||||
native_options,
|
||||
Box::new(|cc| {
|
||||
let config_cl = config_arc.clone();
|
||||
|
||||
let ctx_repainter = cc.egui_ctx.clone();
|
||||
std::thread::spawn(move || loop {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
ctx_repainter.request_repaint();
|
||||
});
|
||||
|
||||
let ctx_komorebi = cc.egui_ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
|
||||
|
||||
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
|
||||
filter_state_changes: true,
|
||||
})
|
||||
.expect("could not subscribe to komorebi notifications");
|
||||
|
||||
tracing::info!("subscribed to komorebi notifications: \"{}\"", subscriber_name);
|
||||
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(subscription) => {
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(subscription);
|
||||
|
||||
// this is when we know a shutdown has been sent
|
||||
if matches!(reader.read_to_end(&mut buffer), Ok(0)) {
|
||||
tracing::info!("disconnected from komorebi");
|
||||
|
||||
// keep trying to reconnect to komorebi
|
||||
while komorebi_client::send_message(
|
||||
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
|
||||
tracing::info!("reconnected to komorebi");
|
||||
|
||||
if let Some(rect) = &config_cl.monitor.work_area_offset {
|
||||
while komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
config_cl.monitor.index,
|
||||
*rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match String::from_utf8(buffer) {
|
||||
Ok(notification_string) => {
|
||||
match serde_json::from_str::<komorebi_client::Notification>(
|
||||
¬ification_string,
|
||||
) {
|
||||
Ok(notification) => {
|
||||
tracing::debug!("received notification from komorebi");
|
||||
|
||||
if let Err(error) = tx_gui.send(notification) {
|
||||
tracing::error!("could not send komorebi notification update to gui thread: {error}")
|
||||
}
|
||||
|
||||
ctx_komorebi.request_repaint();
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("could not deserialize komorebi notification: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"komorebi notification string was invalid utf8: {error}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config_arc)))
|
||||
}),
|
||||
)
|
||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||
}
|
||||
128
komorebi-bar/src/media.rs
Normal file
128
komorebi-bar/src/media.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MediaConfig {
|
||||
/// Enable the Media widget
|
||||
pub enable: bool,
|
||||
}
|
||||
|
||||
impl From<MediaConfig> for Media {
|
||||
fn from(value: MediaConfig) -> Self {
|
||||
Self::new(value.enable)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Media {
|
||||
pub enable: bool,
|
||||
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
|
||||
}
|
||||
|
||||
impl Media {
|
||||
pub fn new(enable: bool) -> Self {
|
||||
Self {
|
||||
enable,
|
||||
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
|
||||
.unwrap()
|
||||
.get()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
||||
if let Ok(op) = session.TryTogglePlayPauseAsync() {
|
||||
op.get().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn output(&mut self) -> String {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
||||
if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
|
||||
if let Ok(properties) = operation.get() {
|
||||
if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
|
||||
if artist.is_empty() {
|
||||
return format!("{title}");
|
||||
}
|
||||
|
||||
if title.is_empty() {
|
||||
return format!("{artist}");
|
||||
}
|
||||
|
||||
return format!("{artist} - {title}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Media {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
egui_phosphor::regular::HEADPHONES.to_string(),
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
if custom_ui
|
||||
.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click())
|
||||
.truncate(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.toggle();
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
komorebi-bar/src/memory.rs
Normal file
123
komorebi-bar/src/memory.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::RefreshKind;
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MemoryConfig {
|
||||
/// Enable the Memory widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<MemoryConfig> for Memory {
|
||||
fn from(value: MemoryConfig) -> Self {
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
|
||||
|
||||
system.refresh_memory();
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Memory {
|
||||
pub enable: bool,
|
||||
system: System,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
fn output(&mut self) -> String {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.system.refresh_memory();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let used = self.system.used_memory();
|
||||
let total = self.system.total_memory();
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Memory {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::MEMORY.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
425
komorebi-bar/src/network.rs
Normal file
425
komorebi-bar/src/network.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use num_derive::FromPrimitive;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Networks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NetworkConfig {
|
||||
/// Enable the Network widget
|
||||
pub enable: bool,
|
||||
/// Show total data transmitted
|
||||
pub show_total_data_transmitted: bool,
|
||||
/// Show network activity
|
||||
pub show_network_activity: bool,
|
||||
/// Characters to reserve for network activity data
|
||||
pub network_activity_fill_characters: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<NetworkConfig> for Network {
|
||||
fn from(value: NetworkConfig) -> Self {
|
||||
let mut last_state_data = vec![];
|
||||
let mut last_state_transmitted = vec![];
|
||||
|
||||
let mut networks_total_data_transmitted = Networks::new_with_refreshed_list();
|
||||
let mut networks_network_activity = Networks::new_with_refreshed_list();
|
||||
|
||||
let mut default_interface = String::new();
|
||||
|
||||
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = interface.friendly_name {
|
||||
default_interface.clone_from(&friendly_name);
|
||||
|
||||
if value.show_total_data_transmitted {
|
||||
networks_total_data_transmitted.refresh();
|
||||
for (interface_name, data) in &networks_total_data_transmitted {
|
||||
if friendly_name.eq(interface_name) {
|
||||
last_state_data.push(match prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{} | {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {} | {} {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"\u{2211}DOWN: {} | \u{2211}UP: {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::IconAndText => format!(
|
||||
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if value.show_network_activity {
|
||||
networks_network_activity.refresh();
|
||||
for (interface_name, data) in &networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
last_state_transmitted.push(match prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{: >width$}/s | {: >width$}/s",
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {: >width$}/s | {} {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"DOWN: {: >width$}/s | UP: {: >width$}/s",
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
LabelPrefix::IconAndText => format!(
|
||||
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
networks_total_data_transmitted,
|
||||
networks_network_activity,
|
||||
default_interface,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: prefix,
|
||||
show_total_data_transmitted: value.show_total_data_transmitted,
|
||||
show_network_activity: value.show_network_activity,
|
||||
network_activity_fill_characters: value
|
||||
.network_activity_fill_characters
|
||||
.unwrap_or_default(),
|
||||
last_state_total_data_transmitted: last_state_data,
|
||||
last_state_network_activity: last_state_transmitted,
|
||||
last_updated_total_data_transmitted: Instant::now(),
|
||||
last_updated_network_activity: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Network {
|
||||
pub enable: bool,
|
||||
pub show_total_data_transmitted: bool,
|
||||
pub show_network_activity: bool,
|
||||
networks_total_data_transmitted: Networks,
|
||||
networks_network_activity: Networks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
default_interface: String,
|
||||
last_state_total_data_transmitted: Vec<String>,
|
||||
last_state_network_activity: Vec<String>,
|
||||
last_updated_total_data_transmitted: Instant,
|
||||
last_updated_network_activity: Instant,
|
||||
network_activity_fill_characters: usize,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
fn default_interface(&mut self) {
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> Vec<String> {
|
||||
let mut outputs = self.last_state_network_activity.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if self.show_network_activity
|
||||
&& now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
outputs.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
if self.show_network_activity {
|
||||
self.networks_network_activity.refresh();
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
outputs.push(match self.label_prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{: >width$}/s | {: >width$}/s",
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {: >width$}/s | {} {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"DOWN: {: >width$}/s | UP: {: >width$}/s",
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
LabelPrefix::IconAndText => {
|
||||
format!(
|
||||
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_network_activity.clone_from(&outputs);
|
||||
self.last_updated_network_activity = now;
|
||||
}
|
||||
|
||||
outputs
|
||||
}
|
||||
|
||||
fn total_data_transmitted(&mut self) -> Vec<String> {
|
||||
let mut outputs = self.last_state_total_data_transmitted.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if self.show_total_data_transmitted
|
||||
&& now.duration_since(self.last_updated_total_data_transmitted)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
outputs.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
if self.show_total_data_transmitted {
|
||||
self.networks_total_data_transmitted.refresh();
|
||||
|
||||
for (interface_name, data) in &self.networks_total_data_transmitted {
|
||||
if friendly_name.eq(interface_name) {
|
||||
outputs.push(match self.label_prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{} | {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {} | {} {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"\u{2211}DOWN: {} | \u{2211}UP: {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::IconAndText => format!(
|
||||
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_total_data_transmitted.clone_from(&outputs);
|
||||
self.last_updated_total_data_transmitted = now;
|
||||
}
|
||||
|
||||
outputs
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Network {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.show_total_data_transmitted {
|
||||
for output in self.total_data_transmitted() {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if self.show_network_activity {
|
||||
for output in self.network_activity() {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if self.enable {
|
||||
self.default_interface();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::WIFI_HIGH.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
self.default_interface.insert_str(0, "NET: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&self.default_interface,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPrimitive)]
|
||||
enum DataUnit {
|
||||
B = 0,
|
||||
K = 1,
|
||||
M = 2,
|
||||
G = 3,
|
||||
T = 4,
|
||||
P = 5,
|
||||
E = 6,
|
||||
Z = 7,
|
||||
Y = 8,
|
||||
}
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
|
||||
let input = input_in_bytes as f32 / timespan_in_s as f32;
|
||||
let mut magnitude = input.log(1024f32) as u32;
|
||||
|
||||
// let the base unit be KiB
|
||||
if magnitude < 1 {
|
||||
magnitude = 1;
|
||||
}
|
||||
|
||||
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
|
||||
let result = input / ((1u64) << (magnitude * 10)) as f32;
|
||||
|
||||
match base {
|
||||
Some(DataUnit::B) => format!("{result:.1} B"),
|
||||
Some(unit) => format!("{result:.1} {unit}iB"),
|
||||
None => String::from("Unknown data unit"),
|
||||
}
|
||||
}
|
||||
134
komorebi-bar/src/storage.rs
Normal file
134
komorebi-bar/src/storage.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Disks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StorageConfig {
|
||||
/// Enable the Storage widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<StorageConfig> for Storage {
|
||||
fn from(value: StorageConfig) -> Self {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
disks: Disks::new_with_refreshed_list(),
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Storage {
|
||||
pub enable: bool,
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
fn output(&mut self) -> Vec<String> {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.disks.refresh();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let mut disks = vec![];
|
||||
|
||||
for disk in &self.disks {
|
||||
let mount = disk.mount_point();
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
let used = total - available;
|
||||
|
||||
disks.push(match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
})
|
||||
}
|
||||
|
||||
disks.sort();
|
||||
disks.reverse();
|
||||
|
||||
disks
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Storage {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
for output in self.output() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::HARD_DRIVES.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
komorebi-bar/src/time.rs
Normal file
128
komorebi-bar/src/time.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TimeConfig {
|
||||
/// Enable the Time widget
|
||||
pub enable: bool,
|
||||
/// Set the Time format
|
||||
pub format: TimeFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<TimeConfig> for Time {
|
||||
fn from(value: TimeConfig) -> Self {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum TimeFormat {
|
||||
/// Twelve-hour format (with seconds)
|
||||
TwelveHour,
|
||||
/// Twenty-four-hour format (with seconds)
|
||||
TwentyFourHour,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl TimeFormat {
|
||||
pub fn toggle(&mut self) {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
|
||||
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn fmt_string(&self) -> String {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
|
||||
TimeFormat::TwentyFourHour => String::from("%T"),
|
||||
TimeFormat::Custom(format) => format.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Time {
|
||||
pub enable: bool,
|
||||
pub format: TimeFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
}
|
||||
|
||||
impl Time {
|
||||
fn output(&mut self) -> String {
|
||||
chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Time {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CLOCK.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.insert_str(0, "TIME: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
komorebi-bar/src/ui.rs
Normal file
22
komorebi-bar/src/ui.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Layout;
|
||||
use eframe::egui::Response;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::Widget;
|
||||
|
||||
pub struct CustomUi<'ui>(pub &'ui mut Ui);
|
||||
|
||||
impl CustomUi<'_> {
|
||||
pub fn add_sized_left_to_right(
|
||||
&mut self,
|
||||
max_size: impl Into<Vec2>,
|
||||
widget: impl Widget,
|
||||
) -> Response {
|
||||
let layout =
|
||||
Layout::from_main_dir_and_cross_align(self.0.layout().main_dir(), Align::Center);
|
||||
self.0
|
||||
.allocate_ui_with_layout(max_size.into(), layout, |ui| ui.add(widget))
|
||||
.inner
|
||||
}
|
||||
}
|
||||
56
komorebi-bar/src/widget.rs
Normal file
56
komorebi-bar/src/widget.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::battery::Battery;
|
||||
use crate::battery::BatteryConfig;
|
||||
use crate::cpu::Cpu;
|
||||
use crate::cpu::CpuConfig;
|
||||
use crate::date::Date;
|
||||
use crate::date::DateConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiConfig;
|
||||
use crate::media::Media;
|
||||
use crate::media::MediaConfig;
|
||||
use crate::memory::Memory;
|
||||
use crate::memory::MemoryConfig;
|
||||
use crate::network::Network;
|
||||
use crate::network::NetworkConfig;
|
||||
use crate::storage::Storage;
|
||||
use crate::storage::StorageConfig;
|
||||
use crate::time::Time;
|
||||
use crate::time::TimeConfig;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub trait BarWidget {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum WidgetConfig {
|
||||
Battery(BatteryConfig),
|
||||
Cpu(CpuConfig),
|
||||
Date(DateConfig),
|
||||
Komorebi(KomorebiConfig),
|
||||
Media(MediaConfig),
|
||||
Memory(MemoryConfig),
|
||||
Network(NetworkConfig),
|
||||
Storage(StorageConfig),
|
||||
Time(TimeConfig),
|
||||
}
|
||||
|
||||
impl WidgetConfig {
|
||||
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
|
||||
match self {
|
||||
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
|
||||
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
|
||||
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
|
||||
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
|
||||
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
|
||||
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
||||
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
|
||||
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
|
||||
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.27"
|
||||
version = "0.1.30"
|
||||
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"
|
||||
|
||||
uds_windows = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -3,7 +3,33 @@
|
||||
|
||||
pub use komorebi::colour::Colour;
|
||||
pub use komorebi::colour::Rgb;
|
||||
pub use komorebi::config_generation::ApplicationConfiguration;
|
||||
pub use komorebi::container::Container;
|
||||
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
||||
pub use komorebi::core::resolve_home_path;
|
||||
pub use komorebi::core::AnimationStyle;
|
||||
pub use komorebi::core::ApplicationIdentifier;
|
||||
pub use komorebi::core::Arrangement;
|
||||
pub use komorebi::core::Axis;
|
||||
pub use komorebi::core::BorderImplementation;
|
||||
pub use komorebi::core::BorderStyle;
|
||||
pub use komorebi::core::CustomLayout;
|
||||
pub use komorebi::core::CycleDirection;
|
||||
pub use komorebi::core::DefaultLayout;
|
||||
pub use komorebi::core::Direction;
|
||||
pub use komorebi::core::FocusFollowsMouseImplementation;
|
||||
pub use komorebi::core::HidingBehaviour;
|
||||
pub use komorebi::core::Layout;
|
||||
pub use komorebi::core::MoveBehaviour;
|
||||
pub use komorebi::core::OperationBehaviour;
|
||||
pub use komorebi::core::OperationDirection;
|
||||
pub use komorebi::core::Rect;
|
||||
pub use komorebi::core::Sizing;
|
||||
pub use komorebi::core::SocketMessage;
|
||||
pub use komorebi::core::StackbarLabel;
|
||||
pub use komorebi::core::StackbarMode;
|
||||
pub use komorebi::core::StateQuery;
|
||||
pub use komorebi::core::WindowKind;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
pub use komorebi::ring::Ring;
|
||||
pub use komorebi::window::Window;
|
||||
@@ -11,27 +37,15 @@ pub use komorebi::window_manager_event::WindowManagerEvent;
|
||||
pub use komorebi::workspace::Workspace;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::GlobalState;
|
||||
pub use komorebi::KomorebiTheme;
|
||||
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::SubscribeOptions;
|
||||
pub use komorebi::TabsConfig;
|
||||
pub use komorebi_core::Arrangement;
|
||||
pub use komorebi_core::Axis;
|
||||
pub use komorebi_core::BorderStyle;
|
||||
pub use komorebi_core::CustomLayout;
|
||||
pub use komorebi_core::CycleDirection;
|
||||
pub use komorebi_core::DefaultLayout;
|
||||
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::StackbarLabel;
|
||||
pub use komorebi_core::StackbarMode;
|
||||
pub use komorebi_core::WindowKind;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
@@ -46,16 +60,10 @@ 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(())
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
|
||||
@@ -89,3 +97,29 @@ pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
pub fn subscribe_with_options(
|
||||
name: &str,
|
||||
options: SubscribeOptions,
|
||||
) -> 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::AddSubscriberSocketWithOptions(
|
||||
name.to_string(),
|
||||
options,
|
||||
))?;
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.27"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
color-eyre = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
@@ -1,14 +1,15 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.27"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
egui_extras = { version = "0.27" }
|
||||
eframe = "0.27"
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
serde_json = "1"
|
||||
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
random_word = { version = "0.4.3", features = ["en"] }
|
||||
serde_json = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
@@ -27,14 +27,13 @@ fn main() {
|
||||
viewport: ViewportBuilder::default()
|
||||
.with_always_on_top()
|
||||
.with_inner_size([320.0, 500.0]),
|
||||
follow_system_theme: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _ = eframe::run_native(
|
||||
"komorebi-gui",
|
||||
native_options,
|
||||
Box::new(|cc| Box::new(KomorebiGui::new(cc))),
|
||||
Box::new(|cc| Ok(Box::new(KomorebiGui::new(cc)))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,7 +217,7 @@ extern "system" fn enum_window(
|
||||
lparam: windows::Win32::Foundation::LPARAM,
|
||||
) -> windows::Win32::Foundation::BOOL {
|
||||
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
|
||||
let window = Window { hwnd: hwnd.0 };
|
||||
let window = Window::from(hwnd.0 as isize);
|
||||
|
||||
if window.is_window()
|
||||
&& !window.is_miminized()
|
||||
@@ -234,7 +233,8 @@ extern "system" fn enum_window(
|
||||
|
||||
fn json_view_ui(ui: &mut egui::Ui, code: &str) {
|
||||
let language = "json";
|
||||
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
let theme =
|
||||
egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), &ui.ctx().style());
|
||||
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
|
||||
}
|
||||
|
||||
@@ -246,9 +246,7 @@ impl eframe::App for KomorebiGui {
|
||||
ui.set_width(ctx.screen_rect().width());
|
||||
ui.collapsing("Debugging", |ui| {
|
||||
ui.collapsing("Window Rules", |ui| {
|
||||
let window = Window {
|
||||
hwnd: self.debug_hwnd,
|
||||
};
|
||||
let window = Window::from(self.debug_hwnd);
|
||||
|
||||
let label = if let (Ok(title), Ok(exe)) = (window.title(), window.exe()) {
|
||||
format!("{title} ({exe})")
|
||||
|
||||
12
komorebi-themes/Cargo.toml
Normal file
12
komorebi-themes/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "57c38257cb0c6434321320d3746049bd58c34674" }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f579847bf2f552b144361d5a78ed8cf360b55cbb" }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui28"] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
162
komorebi-themes/src/lib.rs
Normal file
162
komorebi-themes/src/lib.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub use base16_egui_themes::Base16;
|
||||
pub use catppuccin_egui;
|
||||
pub use eframe::egui::Color32;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Theme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
name: Catppuccin,
|
||||
accent: Option<CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
name: Base16,
|
||||
accent: Option<Base16Value>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Base16Value {
|
||||
Base00,
|
||||
Base01,
|
||||
Base02,
|
||||
Base03,
|
||||
Base04,
|
||||
Base05,
|
||||
#[default]
|
||||
Base06,
|
||||
Base07,
|
||||
Base08,
|
||||
Base09,
|
||||
Base0A,
|
||||
Base0B,
|
||||
Base0C,
|
||||
Base0D,
|
||||
Base0E,
|
||||
Base0F,
|
||||
}
|
||||
|
||||
impl Base16Value {
|
||||
pub fn color32(&self, theme: Base16) -> Color32 {
|
||||
match self {
|
||||
Base16Value::Base00 => theme.base00(),
|
||||
Base16Value::Base01 => theme.base01(),
|
||||
Base16Value::Base02 => theme.base02(),
|
||||
Base16Value::Base03 => theme.base03(),
|
||||
Base16Value::Base04 => theme.base04(),
|
||||
Base16Value::Base05 => theme.base05(),
|
||||
Base16Value::Base06 => theme.base06(),
|
||||
Base16Value::Base07 => theme.base07(),
|
||||
Base16Value::Base08 => theme.base08(),
|
||||
Base16Value::Base09 => theme.base09(),
|
||||
Base16Value::Base0A => theme.base0a(),
|
||||
Base16Value::Base0B => theme.base0b(),
|
||||
Base16Value::Base0C => theme.base0c(),
|
||||
Base16Value::Base0D => theme.base0d(),
|
||||
Base16Value::Base0E => theme.base0e(),
|
||||
Base16Value::Base0F => theme.base0f(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Catppuccin {
|
||||
Frappe,
|
||||
Latte,
|
||||
Macchiato,
|
||||
Mocha,
|
||||
}
|
||||
|
||||
impl Catppuccin {
|
||||
pub fn as_theme(self) -> catppuccin_egui::Theme {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Catppuccin> for catppuccin_egui::Theme {
|
||||
fn from(val: Catppuccin) -> Self {
|
||||
match val {
|
||||
Catppuccin::Frappe => catppuccin_egui::FRAPPE,
|
||||
Catppuccin::Latte => catppuccin_egui::LATTE,
|
||||
Catppuccin::Macchiato => catppuccin_egui::MACCHIATO,
|
||||
Catppuccin::Mocha => catppuccin_egui::MOCHA,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum CatppuccinValue {
|
||||
Rosewater,
|
||||
Flamingo,
|
||||
Pink,
|
||||
Mauve,
|
||||
Red,
|
||||
Maroon,
|
||||
Peach,
|
||||
Yellow,
|
||||
Green,
|
||||
Teal,
|
||||
Sky,
|
||||
Sapphire,
|
||||
Blue,
|
||||
Lavender,
|
||||
#[default]
|
||||
Text,
|
||||
Subtext1,
|
||||
Subtext0,
|
||||
Overlay2,
|
||||
Overlay1,
|
||||
Overlay0,
|
||||
Surface2,
|
||||
Surface1,
|
||||
Surface0,
|
||||
Base,
|
||||
Mantle,
|
||||
Crust,
|
||||
}
|
||||
|
||||
pub fn color32_compat(rgba: [u8; 4]) -> Color32 {
|
||||
Color32::from_rgba_unmultiplied(rgba[0], rgba[1], rgba[2], rgba[3])
|
||||
}
|
||||
|
||||
impl CatppuccinValue {
|
||||
pub fn color32(&self, theme: catppuccin_egui::Theme) -> Color32 {
|
||||
match self {
|
||||
CatppuccinValue::Rosewater => color32_compat(theme.rosewater.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Flamingo => color32_compat(theme.flamingo.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Pink => color32_compat(theme.pink.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Mauve => color32_compat(theme.mauve.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Red => color32_compat(theme.red.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Maroon => color32_compat(theme.maroon.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Peach => color32_compat(theme.peach.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Yellow => color32_compat(theme.yellow.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Green => color32_compat(theme.green.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Teal => color32_compat(theme.teal.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Sky => color32_compat(theme.sky.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Sapphire => color32_compat(theme.sapphire.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Blue => color32_compat(theme.blue.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Lavender => color32_compat(theme.lavender.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Text => color32_compat(theme.text.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Subtext1 => color32_compat(theme.subtext1.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Subtext0 => color32_compat(theme.subtext0.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Overlay2 => color32_compat(theme.overlay2.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Overlay1 => color32_compat(theme.overlay1.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Overlay0 => color32_compat(theme.overlay0.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Surface2 => color32_compat(theme.surface2.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Surface1 => color32_compat(theme.surface1.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Surface0 => color32_compat(theme.surface0.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Base => color32_compat(theme.base.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Mantle => color32_compat(theme.mantle.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Crust => color32_compat(theme.crust.to_srgba_unmultiplied()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.27"
|
||||
version = "0.1.30"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
@@ -11,43 +11,50 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-core = { path = "../komorebi-core" }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
|
||||
bitflags = { version = "2", features = ["serde"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
clap = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
crossbeam-channel = { workspace = true }
|
||||
crossbeam-utils = { workspace = true }
|
||||
ctrlc = { version = "3", features = ["termination"] }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
getset = "0.1"
|
||||
hex_color = { version = "3", features = ["serde"] }
|
||||
hotwatch = "0.5"
|
||||
lazy_static = "1"
|
||||
hotwatch = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
miow = "0.6"
|
||||
nanoid = "0.4"
|
||||
net2 = "0.2"
|
||||
os_info = "3.8"
|
||||
parking_lot = "0.12"
|
||||
paste = "1"
|
||||
paste = { workspace = true }
|
||||
regex = "1"
|
||||
schemars = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
shadow-rs = { workspace = true }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
sysinfo = { workspace = true }
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uds_windows = "1"
|
||||
which = "6"
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
uds_windows = { workspace = true }
|
||||
which = { workspace = true }
|
||||
widestring = "1"
|
||||
win32-display-data = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.52"
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
[features]
|
||||
deadlock_detection = ["parking_lot/deadlock_detection"]
|
||||
|
||||
3
komorebi/build.rs
Normal file
3
komorebi/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
shadow_rs::new().unwrap();
|
||||
}
|
||||
517
komorebi/src/animation.rs
Normal file
517
komorebi/src/animation.rs
Normal file
@@ -0,0 +1,517 @@
|
||||
use crate::core::AnimationStyle;
|
||||
use crate::core::Rect;
|
||||
use color_eyre::Result;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::f64::consts::PI;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_MANAGER;
|
||||
use crate::ANIMATION_STYLE;
|
||||
|
||||
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(60);
|
||||
|
||||
pub trait Ease {
|
||||
fn evaluate(t: f64) -> f64;
|
||||
}
|
||||
|
||||
pub struct Linear;
|
||||
|
||||
impl Ease for Linear {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInSine;
|
||||
|
||||
impl Ease for EaseInSine {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - f64::cos((t * PI) / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutSine;
|
||||
|
||||
impl Ease for EaseOutSine {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
f64::sin((t * PI) / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutSine;
|
||||
|
||||
impl Ease for EaseInOutSine {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
-(f64::cos(PI * t) - 1.0) / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInQuad;
|
||||
|
||||
impl Ease for EaseInQuad {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutQuad;
|
||||
|
||||
impl Ease for EaseOutQuad {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
(1.0 - t).mul_add(-1.0 - t, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutQuad;
|
||||
|
||||
impl Ease for EaseInOutQuad {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
2.0 * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(2) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInCubic;
|
||||
|
||||
impl Ease for EaseInCubic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutCubic;
|
||||
|
||||
impl Ease for EaseOutCubic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - (1.0 - t).powi(3)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutCubic;
|
||||
|
||||
impl Ease for EaseInOutCubic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
4.0 * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(3) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInQuart;
|
||||
|
||||
impl Ease for EaseInQuart {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t * t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutQuart;
|
||||
|
||||
impl Ease for EaseOutQuart {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - (1.0 - t).powi(4)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutQuart;
|
||||
|
||||
impl Ease for EaseInOutQuart {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
8.0 * t * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(4) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInQuint;
|
||||
|
||||
impl Ease for EaseInQuint {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t * t * t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutQuint;
|
||||
|
||||
impl Ease for EaseOutQuint {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - (1.0 - t).powi(5)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutQuint;
|
||||
|
||||
impl Ease for EaseInOutQuint {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
16.0 * t * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(5) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInExpo;
|
||||
|
||||
impl Ease for EaseInExpo {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
10.0f64.mul_add(t, -10.0).exp2()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutExpo;
|
||||
|
||||
impl Ease for EaseOutExpo {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON {
|
||||
return t;
|
||||
}
|
||||
|
||||
1.0 - (-10.0 * t).exp2()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutExpo;
|
||||
|
||||
impl Ease for EaseInOutExpo {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t == 0.0 || (t - 1.0).abs() < f64::EPSILON {
|
||||
return t;
|
||||
}
|
||||
|
||||
if t < 0.5 {
|
||||
20.0f64.mul_add(t, -10.0).exp2() / 2.0
|
||||
} else {
|
||||
(2.0 - (-20.0f64).mul_add(t, 10.0).exp2()) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInCirc;
|
||||
|
||||
impl Ease for EaseInCirc {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - f64::sqrt(t.mul_add(-t, 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutCirc;
|
||||
|
||||
impl Ease for EaseOutCirc {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
f64::sqrt((t - 1.0).mul_add(-(t - 1.0), 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutCirc;
|
||||
|
||||
impl Ease for EaseInOutCirc {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
(1.0 - f64::sqrt((2.0 * t).mul_add(-(2.0 * t), 1.0))) / 2.0
|
||||
} else {
|
||||
(f64::sqrt(
|
||||
(-2.0f64)
|
||||
.mul_add(t, 2.0)
|
||||
.mul_add(-(-2.0f64).mul_add(t, 2.0), 1.0),
|
||||
) + 1.0)
|
||||
/ 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInBack;
|
||||
|
||||
impl Ease for EaseInBack {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let c1 = 1.70158;
|
||||
let c3 = c1 + 1.0;
|
||||
|
||||
(c3 * t * t).mul_add(t, -c1 * t * t)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutBack;
|
||||
|
||||
impl Ease for EaseOutBack {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let c1: f64 = 1.70158;
|
||||
let c3: f64 = c1 + 1.0;
|
||||
|
||||
c1.mul_add((t - 1.0).powi(2), c3.mul_add((t - 1.0).powi(3), 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutBack;
|
||||
|
||||
impl Ease for EaseInOutBack {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let c1: f64 = 1.70158;
|
||||
let c2: f64 = c1 * 1.525;
|
||||
|
||||
if t < 0.5 {
|
||||
((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0).mul_add(t, -c2)) / 2.0
|
||||
} else {
|
||||
((2.0f64.mul_add(t, -2.0))
|
||||
.powi(2)
|
||||
.mul_add((c2 + 1.0).mul_add(t.mul_add(2.0, -2.0), c2), 2.0))
|
||||
/ 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInElastic;
|
||||
|
||||
impl Ease for EaseInElastic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
let c4 = (2.0 * PI) / 3.0;
|
||||
|
||||
-(10.0f64.mul_add(t, -10.0).exp2()) * f64::sin(t.mul_add(10.0, -10.75) * c4)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutElastic;
|
||||
|
||||
impl Ease for EaseOutElastic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
let c4 = (2.0 * PI) / 3.0;
|
||||
|
||||
(-10.0 * t)
|
||||
.exp2()
|
||||
.mul_add(f64::sin(t.mul_add(10.0, -0.75) * c4), 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutElastic;
|
||||
|
||||
impl Ease for EaseInOutElastic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
let c5 = (2.0 * PI) / 4.5;
|
||||
|
||||
if t < 0.5 {
|
||||
-(20.0f64.mul_add(t, -10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0
|
||||
} else {
|
||||
((-20.0f64).mul_add(t, 10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0
|
||||
+ 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInBounce;
|
||||
|
||||
impl Ease for EaseInBounce {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - EaseOutBounce::evaluate(1.0 - t)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutBounce;
|
||||
|
||||
impl Ease for EaseOutBounce {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let mut time = t;
|
||||
let n1 = 7.5625;
|
||||
let d1 = 2.75;
|
||||
|
||||
if t < 1.0 / d1 {
|
||||
n1 * time * time
|
||||
} else if time < 2.0 / d1 {
|
||||
time -= 1.5 / d1;
|
||||
(n1 * time).mul_add(time, 0.75)
|
||||
} else if time < 2.5 / d1 {
|
||||
time -= 2.25 / d1;
|
||||
(n1 * time).mul_add(time, 0.9375)
|
||||
} else {
|
||||
time -= 2.625 / d1;
|
||||
(n1 * time).mul_add(time, 0.984_375)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutBounce;
|
||||
|
||||
impl Ease for EaseInOutBounce {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
(1.0 - EaseOutBounce::evaluate(2.0f64.mul_add(-t, 1.0))) / 2.0
|
||||
} else {
|
||||
(1.0 + EaseOutBounce::evaluate(2.0f64.mul_add(t, -1.0))) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
fn apply_ease_func(t: f64) -> f64 {
|
||||
let style = *ANIMATION_STYLE.lock();
|
||||
|
||||
match style {
|
||||
AnimationStyle::Linear => Linear::evaluate(t),
|
||||
AnimationStyle::EaseInSine => EaseInSine::evaluate(t),
|
||||
AnimationStyle::EaseOutSine => EaseOutSine::evaluate(t),
|
||||
AnimationStyle::EaseInOutSine => EaseInOutSine::evaluate(t),
|
||||
AnimationStyle::EaseInQuad => EaseInQuad::evaluate(t),
|
||||
AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
|
||||
AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t),
|
||||
AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
|
||||
AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t),
|
||||
AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuart => EaseInOutQuart::evaluate(t),
|
||||
AnimationStyle::EaseInQuint => EaseInQuint::evaluate(t),
|
||||
AnimationStyle::EaseOutQuint => EaseOutQuint::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuint => EaseInOutQuint::evaluate(t),
|
||||
AnimationStyle::EaseInExpo => EaseInExpo::evaluate(t),
|
||||
AnimationStyle::EaseOutExpo => EaseOutExpo::evaluate(t),
|
||||
AnimationStyle::EaseInOutExpo => EaseInOutExpo::evaluate(t),
|
||||
AnimationStyle::EaseInCirc => EaseInCirc::evaluate(t),
|
||||
AnimationStyle::EaseOutCirc => EaseOutCirc::evaluate(t),
|
||||
AnimationStyle::EaseInOutCirc => EaseInOutCirc::evaluate(t),
|
||||
AnimationStyle::EaseInBack => EaseInBack::evaluate(t),
|
||||
AnimationStyle::EaseOutBack => EaseOutBack::evaluate(t),
|
||||
AnimationStyle::EaseInOutBack => EaseInOutBack::evaluate(t),
|
||||
AnimationStyle::EaseInElastic => EaseInElastic::evaluate(t),
|
||||
AnimationStyle::EaseOutElastic => EaseOutElastic::evaluate(t),
|
||||
AnimationStyle::EaseInOutElastic => EaseInOutElastic::evaluate(t),
|
||||
AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t),
|
||||
AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
|
||||
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Animation {
|
||||
pub hwnd: isize,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(hwnd: isize) -> Self {
|
||||
Self { hwnd }
|
||||
}
|
||||
|
||||
/// Returns true if the animation needs to continue
|
||||
pub fn cancel(&mut self) -> bool {
|
||||
if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// should be more than 0
|
||||
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(self.hwnd);
|
||||
let max_duration = Duration::from_secs(1);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(
|
||||
ANIMATION_DURATION.load(Ordering::SeqCst) / 2,
|
||||
));
|
||||
}
|
||||
|
||||
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(self.hwnd);
|
||||
|
||||
ANIMATION_MANAGER.lock().end_cancel(self.hwnd);
|
||||
|
||||
latest_cancel_idx == cancel_idx
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn lerp(start: i32, end: i32, t: f64) -> i32 {
|
||||
let time = apply_ease_func(t);
|
||||
f64::from(end - start)
|
||||
.mul_add(time, f64::from(start))
|
||||
.round() as i32
|
||||
}
|
||||
|
||||
pub fn lerp_rect(start_rect: &Rect, end_rect: &Rect, t: f64) -> Rect {
|
||||
Rect {
|
||||
left: Self::lerp(start_rect.left, end_rect.left, t),
|
||||
top: Self::lerp(start_rect.top, end_rect.top, t),
|
||||
right: Self::lerp(start_rect.right, end_rect.right, t),
|
||||
bottom: Self::lerp(start_rect.bottom, end_rect.bottom, t),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
&mut self,
|
||||
duration: Duration,
|
||||
mut render_callback: impl FnMut(f64) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
if ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
let should_animate = self.cancel();
|
||||
|
||||
if !should_animate {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().start(self.hwnd);
|
||||
|
||||
let target_frame_time = Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
|
||||
let mut progress = 0.0;
|
||||
let animation_start = Instant::now();
|
||||
|
||||
// start animation
|
||||
while progress < 1.0 {
|
||||
// check if animation is cancelled
|
||||
if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) {
|
||||
// cancel animation
|
||||
ANIMATION_MANAGER.lock().cancel(self.hwnd);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame_start = Instant::now();
|
||||
// calculate progress
|
||||
progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
|
||||
render_callback(progress).ok();
|
||||
|
||||
// sleep until next frame
|
||||
let frame_time_elapsed = frame_start.elapsed();
|
||||
|
||||
if frame_time_elapsed < target_frame_time {
|
||||
std::thread::sleep(target_frame_time - frame_time_elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
|
||||
// limit progress to 1.0 if animation took longer
|
||||
if progress > 1.0 {
|
||||
progress = 1.0;
|
||||
}
|
||||
|
||||
// process animation for 1.0 to set target position
|
||||
render_callback(progress)
|
||||
}
|
||||
}
|
||||
108
komorebi/src/animation_manager.rs
Normal file
108
komorebi/src/animation_manager.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub static ANIMATIONS_IN_PROGRESS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct AnimationState {
|
||||
pub in_progress: bool,
|
||||
pub cancel_idx_counter: usize,
|
||||
pub pending_cancel_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AnimationManager {
|
||||
animations: HashMap<isize, AnimationState>,
|
||||
}
|
||||
|
||||
impl Default for AnimationManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
animations: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self, hwnd: isize) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(&hwnd) {
|
||||
animation_state.pending_cancel_count > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_progress(&self, hwnd: isize) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(&hwnd) {
|
||||
animation_state.in_progress
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_cancel(&mut self, hwnd: isize) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.pending_cancel_count += 1;
|
||||
animation_state.cancel_idx_counter += 1;
|
||||
|
||||
// return cancel idx
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn latest_cancel_idx(&mut self, hwnd: isize) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_cancel(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.pending_cancel_count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, hwnd: isize) {
|
||||
if let Entry::Vacant(e) = self.animations.entry(hwnd) {
|
||||
e.insert(AnimationState {
|
||||
in_progress: true,
|
||||
cancel_idx_counter: 0,
|
||||
pending_cancel_count: 0,
|
||||
});
|
||||
|
||||
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = false;
|
||||
|
||||
if animation_state.pending_cancel_count == 0 {
|
||||
self.animations.remove(&hwnd);
|
||||
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::FOCUSED;
|
||||
use crate::border_manager::FOCUS_STATE;
|
||||
use crate::border_manager::MONOCLE;
|
||||
use crate::border_manager::STACK;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::UNFOCUSED;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
use komorebi_core::BorderStyle;
|
||||
use komorebi_core::Rect;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
@@ -49,10 +47,11 @@ 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>) };
|
||||
let hwnd = hwnd.0 as isize;
|
||||
|
||||
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
|
||||
if class.starts_with("komoborder") {
|
||||
hwnds.push(hwnd.0);
|
||||
hwnds.push(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +71,7 @@ impl From<isize> for Border {
|
||||
|
||||
impl Border {
|
||||
pub const fn hwnd(&self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
}
|
||||
|
||||
pub fn create(id: &str) -> color_eyre::Result<Self> {
|
||||
@@ -94,8 +93,9 @@ impl Border {
|
||||
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
|
||||
let instance = h_module.0 as isize;
|
||||
std::thread::spawn(move || -> color_eyre::Result<()> {
|
||||
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), h_module)?;
|
||||
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
|
||||
let mut msg: MSG = MSG::default();
|
||||
@@ -106,7 +106,8 @@ impl Border {
|
||||
tracing::debug!("border window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
TranslateMessage(&msg);
|
||||
// TODO: error handling
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
@@ -122,7 +123,7 @@ impl Border {
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
WindowsApi::close_window(self.hwnd())
|
||||
WindowsApi::close_window(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn update(&self, rect: &Rect, mut should_invalidate: bool) -> color_eyre::Result<()> {
|
||||
@@ -132,8 +133,8 @@ impl Border {
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
|
||||
|
||||
// Update the position of the border if required
|
||||
if !WindowsApi::window_rect(self.hwnd())?.eq(&rect) {
|
||||
WindowsApi::set_border_pos(self.hwnd(), &rect, HWND((Z_ORDER.load()).into()))?;
|
||||
if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) {
|
||||
WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?;
|
||||
should_invalidate = true;
|
||||
}
|
||||
|
||||
@@ -162,13 +163,13 @@ impl Border {
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
|
||||
// With the rect that we set in Self::update
|
||||
match WindowsApi::window_rect(window) {
|
||||
match WindowsApi::window_rect(window.0 as isize) {
|
||||
Ok(rect) => {
|
||||
// Grab the focus kind for this border
|
||||
let focus_kind = {
|
||||
let window_kind = {
|
||||
FOCUS_STATE
|
||||
.lock()
|
||||
.get(&window.0)
|
||||
.get(&(window.0 as isize))
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused)
|
||||
};
|
||||
@@ -177,12 +178,7 @@ impl Border {
|
||||
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),
|
||||
}),
|
||||
COLORREF(window_kind_colour(window_kind)),
|
||||
);
|
||||
|
||||
let hbrush = WindowsApi::create_solid_brush(0);
|
||||
@@ -199,27 +195,35 @@ impl Border {
|
||||
match STYLE.load() {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
// TODO: error handling
|
||||
let _ =
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
} else {
|
||||
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
// TODO: error handling
|
||||
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => {
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
// TODO: error handling
|
||||
let _ = RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
// TODO: error handling
|
||||
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
DeleteObject(hpen);
|
||||
DeleteObject(hbrush);
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hpen);
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hbrush);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("could not get border rect: {}", error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
EndPaint(window, &ps);
|
||||
// TODO: error handling
|
||||
let _ = EndPaint(window, &ps);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
|
||||
@@ -2,11 +2,21 @@
|
||||
|
||||
mod border;
|
||||
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::WindowKind;
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::Colour;
|
||||
use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
use border::Border;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::BorderStyle;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use schemars::JsonSchema;
|
||||
@@ -17,28 +27,21 @@ use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::Colour;
|
||||
use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
use border::Border;
|
||||
use komorebi_core::WindowKind;
|
||||
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
|
||||
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
|
||||
|
||||
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
|
||||
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
|
||||
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
|
||||
AtomicCell::new(BorderImplementation::Komorebi);
|
||||
pub static ref FOCUSED: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245))));
|
||||
pub static ref UNFOCUSED: AtomicU32 =
|
||||
@@ -46,6 +49,8 @@ lazy_static! {
|
||||
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))));
|
||||
pub static ref FLOATING: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(245, 245, 165))));
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
@@ -54,12 +59,12 @@ lazy_static! {
|
||||
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub struct Notification;
|
||||
pub struct Notification(pub Option<isize>);
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(50))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
@@ -70,8 +75,8 @@ fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification() {
|
||||
if event_tx().try_send(Notification).is_err() {
|
||||
pub fn send_notification(hwnd: Option<isize>) {
|
||||
if event_tx().try_send(Notification(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
@@ -109,6 +114,16 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
||||
match focus_kind {
|
||||
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Stack => STACK.load(Ordering::SeqCst),
|
||||
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
|
||||
WindowKind::Floating => FLOATING.load(Ordering::SeqCst),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
@@ -125,195 +140,307 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
let receiver = event_rx();
|
||||
event_tx().send(Notification)?;
|
||||
event_tx().send(Notification(None))?;
|
||||
|
||||
let mut previous_snapshot = Ring::default();
|
||||
let mut previous_pending_move_op = None;
|
||||
let mut previous_is_paused = false;
|
||||
let mut previous_notification: Option<Notification> = None;
|
||||
|
||||
'receiver: for _ in receiver {
|
||||
'receiver: for notification in receiver {
|
||||
// Check the wm state every time we receive a notification
|
||||
let state = wm.lock();
|
||||
let is_paused = state.is_paused;
|
||||
let focused_monitor_idx = state.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
state.monitors.elements()[focused_monitor_idx].focused_workspace_idx();
|
||||
let monitors = state.monitors.clone();
|
||||
let pending_move_op = state.pending_move_op;
|
||||
let floating_window_hwnds = state.monitors.elements()[focused_monitor_idx].workspaces()
|
||||
[focused_workspace_idx]
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
drop(state);
|
||||
|
||||
let mut should_process_notification = true;
|
||||
|
||||
if monitors == previous_snapshot
|
||||
// handle the window dragging edge case
|
||||
&& pending_move_op == previous_pending_move_op
|
||||
{
|
||||
should_process_notification = false;
|
||||
}
|
||||
|
||||
// handle the pause edge case
|
||||
if is_paused && !previous_is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the unpause edge case
|
||||
if previous_is_paused && !is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the retile edge case
|
||||
if !should_process_notification && BORDER_STATE.lock().is_empty() {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
let mut borders_monitors = BORDERS_MONITORS.lock();
|
||||
|
||||
// If borders are disabled
|
||||
if !BORDER_ENABLED.load_consume()
|
||||
// Or if the wm is paused
|
||||
|| is_paused
|
||||
// Or if we are handling an alt-tab across workspaces
|
||||
|| ALT_TAB_HWND.load().is_some()
|
||||
{
|
||||
// Destroy the borders we know about
|
||||
for (_, border) in borders.iter() {
|
||||
border.destroy()?;
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
|
||||
previous_is_paused = is_paused;
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
'monitors: for (monitor_idx, m) in monitors.elements().iter().enumerate() {
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Workspaces with tiling disabled don't have borders
|
||||
if !ws.tile() {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let border = match borders.entry(monocle.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(monocle.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(
|
||||
border.hwnd,
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
match IMPLEMENTATION.load() {
|
||||
BorderImplementation::Windows => {
|
||||
'monitors: for (monitor_idx, m) in monitors.elements().iter().enumerate() {
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let window_kind = if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
monocle
|
||||
.focused_window()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
|
||||
border.update(&rect, true)?;
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
let mut to_remove = vec![];
|
||||
for (id, b) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
|
||||
&& border_hwnd != b.hwnd
|
||||
{
|
||||
b.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
let window_kind = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
} else if c.windows().len() > 1 {
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
WindowKind::Single
|
||||
};
|
||||
|
||||
c.focused_window()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BorderImplementation::Komorebi => {
|
||||
let mut should_process_notification = true;
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
if monitors == previous_snapshot
|
||||
// handle the window dragging edge case
|
||||
&& pending_move_op == previous_pending_move_op
|
||||
{
|
||||
should_process_notification = false;
|
||||
}
|
||||
|
||||
let is_maximized = WindowsApi::is_zoomed(HWND(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
));
|
||||
// handle the pause edge case
|
||||
if is_paused && !previous_is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
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());
|
||||
// handle the unpause edge case
|
||||
if previous_is_paused && !is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the retile edge case
|
||||
if !should_process_notification && BORDER_STATE.lock().is_empty() {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// when we switch focus to a floating window
|
||||
if !should_process_notification
|
||||
&& floating_window_hwnds.contains(¬ification.0.unwrap_or_default())
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
if let Some(ref previous) = previous_notification {
|
||||
if previous.0.unwrap_or_default() != notification.0.unwrap_or_default() {
|
||||
should_process_notification = true;
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Destroy any borders not associated with the focused workspace
|
||||
let container_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
.map(|c| c.id().clone())
|
||||
.collect::<Vec<_>>();
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
let mut borders_monitors = BORDERS_MONITORS.lock();
|
||||
|
||||
// If borders are disabled
|
||||
if !BORDER_ENABLED.load_consume()
|
||||
// Or if they are temporarily disabled
|
||||
|| BORDER_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
|
||||
// Or if the wm is paused
|
||||
|| is_paused
|
||||
// Or if we are handling an alt-tab across workspaces
|
||||
|| ALT_TAB_HWND.load().is_some()
|
||||
{
|
||||
// Destroy the borders we know about
|
||||
for (_, border) in borders.iter() {
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
|
||||
previous_is_paused = is_paused;
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
'monitors: for (monitor_idx, m) in monitors.elements().iter().enumerate() {
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Workspaces with tiling disabled don't have borders
|
||||
if !ws.tile() {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default()
|
||||
== monitor_idx
|
||||
{
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
// Update border when moving or resizing with mouse
|
||||
if pending_move_op.is_some() && idx == ws.focused_container_idx() {
|
||||
let restore_z_order = Z_ORDER.load();
|
||||
Z_ORDER.store(ZOrder::TopMost);
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
let mut rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
while WindowsApi::lbutton_is_pressed() {
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let border = match borders.entry(monocle.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(monocle.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(
|
||||
border.hwnd,
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
border.update(&rect, true)?;
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
let mut to_remove = vec![];
|
||||
for (id, b) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default()
|
||||
== monitor_idx
|
||||
&& border_hwnd != b.hwnd
|
||||
{
|
||||
b.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let is_maximized = WindowsApi::is_zoomed(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
);
|
||||
|
||||
if is_maximized {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default()
|
||||
== monitor_idx
|
||||
{
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Destroy any borders not associated with the focused workspace
|
||||
let mut container_and_floating_window_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
.map(|c| c.id().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for w in ws.floating_windows() {
|
||||
container_and_floating_window_ids.push(w.hwnd.to_string());
|
||||
}
|
||||
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
|
||||
&& !container_and_floating_window_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() {
|
||||
let hwnd = c.focused_window().copied().unwrap_or_default().hwnd;
|
||||
let notification_hwnd = notification.0.unwrap_or_default();
|
||||
|
||||
// Update border when moving or resizing with mouse
|
||||
if pending_move_op.is_some()
|
||||
&& idx == ws.focused_container_idx()
|
||||
&& hwnd == notification_hwnd
|
||||
{
|
||||
let restore_z_order = Z_ORDER.load();
|
||||
Z_ORDER.store(ZOrder::TopMost);
|
||||
|
||||
let mut rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
while WindowsApi::lbutton_is_pressed() {
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(c.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
if rect != new_rect {
|
||||
rect = new_rect;
|
||||
border.update(&rect, true)?;
|
||||
}
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Get the border entry for this container from the map or create one
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
@@ -325,64 +452,134 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
};
|
||||
|
||||
let new_rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
borders_monitors.insert(c.id().clone(), monitor_idx);
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
|
||||
let new_focus_state = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
} else if c.windows().len() > 1 {
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
WindowKind::Single
|
||||
};
|
||||
|
||||
// Update the focused state for all containers on this workspace
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
last_focus_state = focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
if rect != new_rect {
|
||||
rect = new_rect;
|
||||
border.update(&rect, true)?;
|
||||
}
|
||||
let should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
{
|
||||
let restore_z_order = Z_ORDER.load();
|
||||
Z_ORDER.store(ZOrder::TopMost);
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
'windows: for window in ws.floating_windows() {
|
||||
let hwnd = window.hwnd;
|
||||
let notification_hwnd = notification.0.unwrap_or_default();
|
||||
|
||||
// Get the border entry for this container from the map or create one
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(c.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
if pending_move_op.is_some() && hwnd == notification_hwnd {
|
||||
let mut rect = WindowsApi::window_rect(hwnd)?;
|
||||
|
||||
while WindowsApi::lbutton_is_pressed() {
|
||||
let border = match borders.entry(hwnd.to_string()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) =
|
||||
Border::create(&hwnd.to_string())
|
||||
{
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_rect = WindowsApi::window_rect(hwnd)?;
|
||||
|
||||
if rect != new_rect {
|
||||
rect = new_rect;
|
||||
border.update(&rect, true)?;
|
||||
}
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let border = match borders.entry(window.hwnd.to_string()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(&window.hwnd.to_string())
|
||||
{
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
|
||||
|
||||
let mut should_destroy = false;
|
||||
|
||||
if let Some(notification_hwnd) = notification.0 {
|
||||
if notification_hwnd != window.hwnd {
|
||||
should_destroy = true;
|
||||
}
|
||||
}
|
||||
|
||||
if WindowsApi::foreground_window().unwrap_or_default()
|
||||
!= window.hwnd
|
||||
{
|
||||
should_destroy = true;
|
||||
}
|
||||
|
||||
if should_destroy {
|
||||
border.destroy()?;
|
||||
borders.remove(&window.hwnd.to_string());
|
||||
borders_monitors.remove(&window.hwnd.to_string());
|
||||
continue 'windows;
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
let new_focus_state = WindowKind::Floating;
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
last_focus_state =
|
||||
focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
|
||||
let should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(c.id().clone(), monitor_idx);
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
|
||||
let new_focus_state = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
} else if c.windows().len() > 1 {
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
WindowKind::Single
|
||||
};
|
||||
|
||||
// Update the focused state for all containers on this workspace
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
last_focus_state = focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
|
||||
let should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,6 +587,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
previous_snapshot = monitors;
|
||||
previous_pending_move_op = pending_move_op;
|
||||
previous_is_paused = is_paused;
|
||||
previous_notification = Some(notification);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use hex_color::HexColor;
|
||||
use komorebi_themes::Color32;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::InstanceType;
|
||||
use schemars::schema::Schema;
|
||||
@@ -28,6 +29,16 @@ impl From<u32> for Colour {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color32> for Colour {
|
||||
fn from(value: Color32) -> Self {
|
||||
Colour::Rgb(Rgb::new(
|
||||
value.r() as u32,
|
||||
value.g() as u32,
|
||||
value.b() as u32,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct Hex(HexColor);
|
||||
|
||||
@@ -39,6 +50,7 @@ impl JsonSchema for Hex {
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
format: Some("color-hex".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
|
||||
@@ -10,13 +10,13 @@ use interfaces::IServiceProvider;
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use windows::core::Interface;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::System::Com::CoCreateInstance;
|
||||
use windows::Win32::System::Com::CoInitializeEx;
|
||||
use windows::Win32::System::Com::CoUninitialize;
|
||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||
use windows::Win32::System::Com::COINIT_APARTMENTTHREADED;
|
||||
use windows_core::Interface;
|
||||
|
||||
struct ComInit();
|
||||
|
||||
@@ -77,7 +77,7 @@ pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
|
||||
if view_collection.get_view_for_hwnd(hwnd, &mut view).is_err() {
|
||||
tracing::error!(
|
||||
"could not get view for hwnd {} due to os error: {}",
|
||||
hwnd.0,
|
||||
hwnd.0 as isize,
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
@@ -85,14 +85,14 @@ pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
|
||||
|
||||
view.map_or_else(
|
||||
|| {
|
||||
tracing::error!("no view was found for {}", hwnd.0,);
|
||||
tracing::error!("no view was found for {}", hwnd.0 as isize);
|
||||
},
|
||||
|view| {
|
||||
unsafe {
|
||||
if view.set_cloak(cloak_type, flags).is_err() {
|
||||
tracing::error!(
|
||||
"could not change the cloaking status for hwnd {} due to os error: {}",
|
||||
hwnd.0,
|
||||
hwnd.0 as isize,
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde::Serialize;
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, JsonSchema)]
|
||||
pub struct Container {
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
@@ -27,12 +27,6 @@ impl Default for Container {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Container {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn hide(&self, omit: Option<isize>) {
|
||||
for window in self.windows().iter().rev() {
|
||||
@@ -116,6 +110,13 @@ impl Container {
|
||||
pub fn add_window(&mut self, window: Window) {
|
||||
self.windows_mut().push_back(window);
|
||||
self.focus_window(self.windows().len().saturating_sub(1));
|
||||
let focused_window_idx = self.focused_window_idx();
|
||||
|
||||
for (i, window) in self.windows().iter().enumerate() {
|
||||
if i != focused_window_idx {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
|
||||
42
komorebi/src/core/animation.rs
Normal file
42
komorebi/src/core/animation.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use clap::ValueEnum;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
pub enum AnimationStyle {
|
||||
Linear,
|
||||
EaseInSine,
|
||||
EaseOutSine,
|
||||
EaseInOutSine,
|
||||
EaseInQuad,
|
||||
EaseOutQuad,
|
||||
EaseInOutQuad,
|
||||
EaseInCubic,
|
||||
EaseInOutCubic,
|
||||
EaseInQuart,
|
||||
EaseOutQuart,
|
||||
EaseInOutQuart,
|
||||
EaseInQuint,
|
||||
EaseOutQuint,
|
||||
EaseInOutQuint,
|
||||
EaseInExpo,
|
||||
EaseOutExpo,
|
||||
EaseInOutExpo,
|
||||
EaseInCirc,
|
||||
EaseOutCirc,
|
||||
EaseInOutCirc,
|
||||
EaseInBack,
|
||||
EaseOutBack,
|
||||
EaseInOutBack,
|
||||
EaseInElastic,
|
||||
EaseOutElastic,
|
||||
EaseInOutElastic,
|
||||
EaseInBounce,
|
||||
EaseOutBounce,
|
||||
EaseInOutBounce,
|
||||
}
|
||||
@@ -7,12 +7,12 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::custom_layout::Column;
|
||||
use crate::custom_layout::ColumnSplit;
|
||||
use crate::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Rect;
|
||||
use super::custom_layout::Column;
|
||||
use super::custom_layout::ColumnSplit;
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Rect;
|
||||
|
||||
pub trait Arrangement {
|
||||
fn calculate(
|
||||
@@ -6,7 +6,7 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::ApplicationIdentifier;
|
||||
use super::ApplicationIdentifier;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
@@ -59,6 +59,14 @@ pub enum MatchingRule {
|
||||
Composite(Vec<IdWithIdentifier>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WorkspaceMatchingRule {
|
||||
pub monitor_index: usize,
|
||||
pub workspace_index: usize,
|
||||
pub matching_rule: MatchingRule,
|
||||
pub initial_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct IdWithIdentifier {
|
||||
pub kind: ApplicationIdentifier,
|
||||
@@ -108,7 +116,8 @@ 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<MatchingRule>>,
|
||||
#[serde(alias = "float_identifiers")]
|
||||
pub ignore_identifiers: Option<Vec<MatchingRule>>,
|
||||
}
|
||||
|
||||
impl ApplicationConfiguration {
|
||||
@@ -179,7 +188,7 @@ impl ApplicationConfigurationGenerator {
|
||||
|
||||
let mut lines = vec![String::from("# Generated by komorebic.exe"), String::new()];
|
||||
|
||||
let mut float_rules = vec![];
|
||||
let mut ignore_rules = vec![];
|
||||
|
||||
for app in cfgen {
|
||||
lines.push(format!("# {}", app.name));
|
||||
@@ -193,15 +202,15 @@ impl ApplicationConfigurationGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(float_identifiers) = app.float_identifiers {
|
||||
for matching_rule in float_identifiers {
|
||||
if let Some(ignore_identifiers) = app.ignore_identifiers {
|
||||
for matching_rule in ignore_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());
|
||||
if !ignore_rules.contains(&float_rule) {
|
||||
ignore_rules.push(float_rule.clone());
|
||||
|
||||
// if let Some(comment) = float.comment {
|
||||
// lines.push(format!("# {comment}"));
|
||||
@@ -230,7 +239,7 @@ impl ApplicationConfigurationGenerator {
|
||||
|
||||
let mut lines = vec![String::from("; Generated by komorebic.exe"), String::new()];
|
||||
|
||||
let mut float_rules = vec![];
|
||||
let mut ignore_rules = vec![];
|
||||
|
||||
for app in cfgen {
|
||||
lines.push(format!("; {}", app.name));
|
||||
@@ -244,8 +253,8 @@ impl ApplicationConfigurationGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(float_identifiers) = app.float_identifiers {
|
||||
for matching_rule in float_identifiers {
|
||||
if let Some(ignore_identifiers) = app.ignore_identifiers {
|
||||
for matching_rule in ignore_identifiers {
|
||||
if let MatchingRule::Simple(float) = matching_rule {
|
||||
let float_rule = format!(
|
||||
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
|
||||
@@ -253,8 +262,8 @@ impl ApplicationConfigurationGenerator {
|
||||
);
|
||||
|
||||
// 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 !ignore_rules.contains(&float_rule) {
|
||||
ignore_rules.push(float_rule.clone());
|
||||
|
||||
// if let Some(comment) = float.comment {
|
||||
// lines.push(format!("; {comment}"));
|
||||
@@ -12,7 +12,7 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Rect;
|
||||
use super::Rect;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct CustomLayout(Vec<Column>);
|
||||
@@ -5,9 +5,9 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::OperationDirection;
|
||||
use crate::Rect;
|
||||
use crate::Sizing;
|
||||
use super::OperationDirection;
|
||||
use super::Rect;
|
||||
use super::Sizing;
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
@@ -35,6 +35,37 @@ pub enum DefaultLayout {
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
Self::UltrawideVerticalStack | Self::RightMainVerticalStack => match len {
|
||||
n if n > 1 => 1,
|
||||
_ => 0,
|
||||
},
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::HorizontalStack
|
||||
| DefaultLayout::Grid => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rightmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::HorizontalStack
|
||||
| DefaultLayout::Grid => len.saturating_sub(1),
|
||||
DefaultLayout::UltrawideVerticalStack => match len {
|
||||
2 => 0,
|
||||
_ => len.saturating_sub(1),
|
||||
},
|
||||
DefaultLayout::RightMainVerticalStack => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
|
||||
pub fn resize(
|
||||
@@ -164,14 +195,14 @@ impl DefaultLayout {
|
||||
#[must_use]
|
||||
pub const fn cycle_previous(self) -> Self {
|
||||
match self {
|
||||
Self::BSP => Self::UltrawideVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::Rows,
|
||||
Self::Rows => Self::Columns,
|
||||
Self::Columns => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
Self::Columns => Self::BSP,
|
||||
Self::BSP => Self::RightMainVerticalStack,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::custom_layout::Column;
|
||||
use crate::custom_layout::ColumnSplit;
|
||||
use crate::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::custom_layout::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::OperationDirection;
|
||||
use super::custom_layout::Column;
|
||||
use super::custom_layout::ColumnSplit;
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use super::custom_layout::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::OperationDirection;
|
||||
|
||||
pub trait Direction {
|
||||
fn index_in_direction(
|
||||
@@ -2,10 +2,10 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Arrangement;
|
||||
use crate::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Direction;
|
||||
use super::Arrangement;
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Direction;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum Layout {
|
||||
@@ -14,6 +14,7 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
pub use custom_layout::CustomLayout;
|
||||
@@ -24,6 +25,7 @@ pub use layout::Layout;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
pub mod arrangement;
|
||||
pub mod config_generation;
|
||||
pub mod custom_layout;
|
||||
@@ -43,12 +45,13 @@ pub enum SocketMessage {
|
||||
CycleFocusWindow(CycleDirection),
|
||||
CycleMoveWindow(CycleDirection),
|
||||
StackWindow(OperationDirection),
|
||||
UnstackWindow,
|
||||
CycleStack(CycleDirection),
|
||||
FocusStackWindow(usize),
|
||||
StackAll,
|
||||
UnstackAll,
|
||||
ResizeWindowEdge(OperationDirection, Sizing),
|
||||
ResizeWindowAxis(Axis, Sizing),
|
||||
UnstackWindow,
|
||||
CycleStack(CycleDirection),
|
||||
MoveContainerToMonitorNumber(usize),
|
||||
CycleMoveContainerToMonitor(CycleDirection),
|
||||
MoveContainerToWorkspaceNumber(usize),
|
||||
@@ -74,6 +77,7 @@ pub enum SocketMessage {
|
||||
ToggleMonocle,
|
||||
ToggleMaximize,
|
||||
ToggleWindowContainerBehaviour,
|
||||
ToggleFloatOverride,
|
||||
WindowHidingBehaviour(HidingBehaviour),
|
||||
ToggleCrossMonitorMoveBehaviour,
|
||||
CrossMonitorMoveBehaviour(MoveBehaviour),
|
||||
@@ -87,6 +91,8 @@ pub enum SocketMessage {
|
||||
CycleLayout(CycleDirection),
|
||||
ChangeLayoutCustom(PathBuf),
|
||||
FlipLayout(Axis),
|
||||
ToggleWorkspaceWindowContainerBehaviour,
|
||||
ToggleWorkspaceFloatOverride,
|
||||
// Monitor and Workspace Commands
|
||||
MonitorIndexPreference(usize, i32, i32, i32, i32),
|
||||
DisplayIndexPreference(usize, String),
|
||||
@@ -130,10 +136,15 @@ pub enum SocketMessage {
|
||||
ClearNamedWorkspaceLayoutRules(String),
|
||||
// Configuration
|
||||
ReloadConfiguration,
|
||||
ReplaceConfiguration(PathBuf),
|
||||
ReloadStaticConfiguration(PathBuf),
|
||||
WatchConfiguration(bool),
|
||||
CompleteConfiguration,
|
||||
AltFocusHack(bool),
|
||||
Animation(bool),
|
||||
AnimationDuration(u64),
|
||||
AnimationFps(u64),
|
||||
AnimationStyle(AnimationStyle),
|
||||
#[serde(alias = "ActiveWindowBorder")]
|
||||
Border(bool),
|
||||
#[serde(alias = "ActiveWindowBorderColour")]
|
||||
@@ -142,7 +153,9 @@ pub enum SocketMessage {
|
||||
BorderStyle(BorderStyle),
|
||||
BorderWidth(i32),
|
||||
BorderOffset(i32),
|
||||
BorderImplementation(BorderImplementation),
|
||||
Transparency(bool),
|
||||
ToggleTransparency,
|
||||
TransparencyAlpha(u8),
|
||||
InvisibleBorders(Rect),
|
||||
StackbarMode(StackbarMode),
|
||||
@@ -152,6 +165,8 @@ pub enum SocketMessage {
|
||||
StackbarBackgroundColour(u32, u32, u32),
|
||||
StackbarHeight(i32),
|
||||
StackbarTabWidth(i32),
|
||||
StackbarFontSize(i32),
|
||||
StackbarFontFamily(Option<String>),
|
||||
WorkAreaOffset(Rect),
|
||||
MonitorWorkAreaOffset(usize, Rect),
|
||||
ResizeDelta(i32),
|
||||
@@ -159,7 +174,11 @@ pub enum SocketMessage {
|
||||
InitialNamedWorkspaceRule(ApplicationIdentifier, String, String),
|
||||
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
NamedWorkspaceRule(ApplicationIdentifier, String, String),
|
||||
FloatRule(ApplicationIdentifier, String),
|
||||
ClearWorkspaceRules(usize, usize),
|
||||
ClearNamedWorkspaceRules(String),
|
||||
ClearAllWorkspaceRules,
|
||||
#[serde(alias = "FloatRule")]
|
||||
IgnoreRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
IdentifyObjectNameChangeApplication(ApplicationIdentifier, String),
|
||||
IdentifyTrayApplication(ApplicationIdentifier, String),
|
||||
@@ -177,6 +196,7 @@ pub enum SocketMessage {
|
||||
RemoveTitleBar(ApplicationIdentifier, String),
|
||||
ToggleTitleBars,
|
||||
AddSubscriberSocket(String),
|
||||
AddSubscriberSocketWithOptions(String, SubscribeOptions),
|
||||
RemoveSubscriberSocket(String),
|
||||
AddSubscriberPipe(String),
|
||||
RemoveSubscriberPipe(String),
|
||||
@@ -202,6 +222,12 @@ impl FromStr for SocketMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct SubscribeOptions {
|
||||
/// Only emit notifications when the window manager state has changed
|
||||
pub filter_state_changes: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum StackbarMode {
|
||||
Always,
|
||||
@@ -219,7 +245,17 @@ pub enum StackbarLabel {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
|
||||
Default,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Display,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
ValueEnum,
|
||||
)]
|
||||
pub enum BorderStyle {
|
||||
#[default]
|
||||
@@ -231,6 +267,27 @@ pub enum BorderStyle {
|
||||
Square,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Display,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
ValueEnum,
|
||||
)]
|
||||
pub enum BorderImplementation {
|
||||
#[default]
|
||||
/// Use the adjustable komorebi border implementation
|
||||
Komorebi,
|
||||
/// Use the thin Windows accent border implementation
|
||||
Windows,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
@@ -248,6 +305,7 @@ pub enum WindowKind {
|
||||
Stack,
|
||||
Monocle,
|
||||
Unfocused,
|
||||
Floating,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -285,7 +343,16 @@ pub enum ApplicationIdentifier {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum FocusFollowsMouseImplementation {
|
||||
/// A custom FFM implementation (slightly more CPU-intensive)
|
||||
@@ -294,18 +361,48 @@ pub enum FocusFollowsMouseImplementation {
|
||||
Windows,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct WindowManagementBehaviour {
|
||||
/// The current WindowContainerBehaviour to be used
|
||||
pub current_behaviour: WindowContainerBehaviour,
|
||||
/// Override of `current_behaviour` to open new windows as floating windows
|
||||
/// that can be later toggled to tiled, when false it will default to
|
||||
/// `current_behaviour` again.
|
||||
pub float_override: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
pub enum WindowContainerBehaviour {
|
||||
/// Create a new container for each new window
|
||||
#[default]
|
||||
Create,
|
||||
/// Append new windows to the focused window container
|
||||
Append,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum MoveBehaviour {
|
||||
/// Swap the window container with the window container at the edge of the adjacent monitor
|
||||
@@ -316,6 +413,16 @@ pub enum MoveBehaviour {
|
||||
NoOp,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
pub enum CrossBoundaryBehaviour {
|
||||
/// Attempt to perform actions across a workspace boundary
|
||||
Workspace,
|
||||
/// Attempt to perform actions across a monitor boundary
|
||||
Monitor,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
@@ -324,12 +431,21 @@ pub enum HidingBehaviour {
|
||||
Hide,
|
||||
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
Minimize,
|
||||
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
|
||||
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||
Cloak,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum OperationBehaviour {
|
||||
/// Process komorebic commands on temporarily unmanaged/floated windows
|
||||
@@ -7,8 +7,8 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::direction::Direction;
|
||||
use crate::Axis;
|
||||
use super::direction::Direction;
|
||||
use super::Axis;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
@@ -84,4 +84,14 @@ impl Rect {
|
||||
bottom: (self.bottom * rect_dpi) / system_dpi,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn rect(&self) -> RECT {
|
||||
RECT {
|
||||
left: self.left,
|
||||
top: self.top,
|
||||
right: self.left + self.right,
|
||||
bottom: self.top + self.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
70
komorebi/src/focus_manager.rs
Normal file
70
komorebi/src/focus_manager.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use parking_lot::Mutex;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::Window;
|
||||
use crate::WindowManager;
|
||||
|
||||
pub struct Notification(isize);
|
||||
|
||||
impl Deref for Notification {
|
||||
type Target = isize;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
// Currently this should only be used for async focus updates, such as
|
||||
// when an animation finishes and we need to focus to set the cursor
|
||||
// position if the user has mouse follows focus enabled
|
||||
pub fn send_notification(hwnd: isize) {
|
||||
if event_tx().try_send(Notification(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
|
||||
for notification in receiver {
|
||||
let mouse_follows_focus = wm.lock().mouse_follows_focus;
|
||||
let _ = Window::from(*notification).focus(mouse_follows_focus);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
pub mod animation;
|
||||
pub mod animation_manager;
|
||||
pub mod border_manager;
|
||||
pub mod com;
|
||||
#[macro_use]
|
||||
pub mod ring;
|
||||
pub mod colour;
|
||||
pub mod container;
|
||||
pub mod core;
|
||||
pub mod focus_manager;
|
||||
pub mod monitor;
|
||||
pub mod monitor_reconciliator;
|
||||
pub mod process_command;
|
||||
@@ -38,10 +42,14 @@ use std::process::Command;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use animation::*;
|
||||
pub use animation_manager::*;
|
||||
pub use colour::*;
|
||||
pub use core::*;
|
||||
pub use process_command::*;
|
||||
pub use process_event::*;
|
||||
pub use static_config::*;
|
||||
@@ -51,14 +59,11 @@ pub use window_manager_event::*;
|
||||
pub use windows_api::WindowsApi;
|
||||
pub use windows_api::*;
|
||||
|
||||
use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use crate::core::config_generation::WorkspaceMatchingRule;
|
||||
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 os_info::Version;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
@@ -70,8 +75,6 @@ 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![
|
||||
@@ -126,16 +129,17 @@ lazy_static! {
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
]));
|
||||
static ref TRANSPARENCY_BLACKLIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
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 WORKSPACE_MATCHING_RULES: Arc<Mutex<Vec<WorkspaceMatchingRule>>> =
|
||||
Arc::new(Mutex::new(Vec::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![
|
||||
static ref IGNORE_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 {
|
||||
@@ -147,9 +151,14 @@ lazy_static! {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: String::from("IHWindowClass"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("komorebi-bar.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
]));
|
||||
|
||||
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
@@ -157,9 +166,18 @@ lazy_static! {
|
||||
"X410.exe".to_string(),
|
||||
"vcxsrv.exe".to_string(),
|
||||
]));
|
||||
static ref SLOW_APPLICATION_IDENTIFIERS: 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),
|
||||
}),
|
||||
]));
|
||||
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
|
||||
pub static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref SUBSCRIPTION_SOCKET_OPTIONS: Arc<Mutex<HashMap<String, SubscribeOptions>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
@@ -197,13 +215,18 @@ lazy_static! {
|
||||
)
|
||||
};
|
||||
|
||||
static ref ANIMATION_STYLE: Arc<Mutex<AnimationStyle >> =
|
||||
Arc::new(Mutex::new(AnimationStyle::Linear));
|
||||
|
||||
static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
|
||||
Arc::new(Mutex::new(AnimationManager::new()));
|
||||
|
||||
// Use app-specific titlebar removal options where possible
|
||||
// eg. Windows Terminal, IntelliJ IDEA, Firefox
|
||||
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
static ref 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);
|
||||
@@ -214,6 +237,10 @@ 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 ANIMATION_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250);
|
||||
|
||||
pub static SLOW_APPLICATION_COMPENSATION_TIME: AtomicU64 = AtomicU64::new(20);
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
@@ -272,18 +299,34 @@ pub struct Notification {
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
|
||||
let is_subscription_event = matches!(
|
||||
notification.event,
|
||||
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
|
||||
| NotificationEvent::Socket(SocketMessage::AddSubscriberSocketWithOptions(_, _))
|
||||
);
|
||||
|
||||
let notification = &serde_json::to_string(¬ification)?;
|
||||
let mut stale_sockets = vec![];
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
let options = SUBSCRIPTION_SOCKET_OPTIONS.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());
|
||||
let apply_state_filter = (*options)
|
||||
.get(socket)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.filter_state_changes;
|
||||
|
||||
if !apply_state_filter || state_has_been_modified || is_subscription_event {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,6 +334,13 @@ pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
for socket in stale_sockets {
|
||||
tracing::warn!("removing stale subscription: {socket}");
|
||||
sockets.remove(&socket);
|
||||
let socket_path = DATA_DIR.join(socket);
|
||||
if let Err(error) = std::fs::remove_file(&socket_path) {
|
||||
tracing::error!(
|
||||
"could not remove stale subscriber socket file at {}: {error}",
|
||||
socket_path.display()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let mut stale_pipes = vec![];
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
clippy::doc_markdown
|
||||
)]
|
||||
|
||||
use std::net::Shutdown;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
@@ -20,11 +21,14 @@ use crossbeam_utils::Backoff;
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
use sysinfo::Process;
|
||||
use sysinfo::ProcessesToUpdate;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use komorebi::border_manager;
|
||||
use komorebi::focus_manager;
|
||||
use komorebi::load_configuration;
|
||||
use komorebi::monitor_reconciliator;
|
||||
use komorebi::process_command::listen_for_commands;
|
||||
@@ -45,6 +49,8 @@ use komorebi::HOME_DIR;
|
||||
use komorebi::INITIAL_CONFIGURATION_LOADED;
|
||||
use komorebi::SESSION_ID;
|
||||
|
||||
shadow_rs::shadow!(build);
|
||||
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
@@ -132,7 +138,7 @@ fn detect_deadlocks() {
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
||||
struct Opts {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(short, long = "ffm")]
|
||||
@@ -162,9 +168,9 @@ fn main() -> Result<()> {
|
||||
SESSION_ID.store(session_id, Ordering::SeqCst);
|
||||
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes();
|
||||
system.refresh_processes(ProcessesToUpdate::All);
|
||||
|
||||
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe").collect();
|
||||
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe".as_ref()).collect();
|
||||
|
||||
if matched_procs.len() > 1 {
|
||||
let mut len = matched_procs.len();
|
||||
@@ -215,6 +221,7 @@ fn main() -> Result<()> {
|
||||
Arc::new(Mutex::new(StaticConfig::preload(
|
||||
config,
|
||||
winevent_listener::event_rx(),
|
||||
None,
|
||||
)?))
|
||||
} else {
|
||||
Arc::new(Mutex::new(WindowManager::new(
|
||||
@@ -263,6 +270,7 @@ fn main() -> Result<()> {
|
||||
workspace_reconciliator::listen_for_notifications(wm.clone());
|
||||
monitor_reconciliator::listen_for_notifications(wm.clone())?;
|
||||
reaper::watch_for_orphans(wm.clone());
|
||||
focus_manager::listen_for_notifications(wm.clone());
|
||||
|
||||
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
||||
ctrlc::set_handler(move || {
|
||||
@@ -283,5 +291,15 @@ fn main() -> Result<()> {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
}
|
||||
|
||||
let sockets = komorebi::SUBSCRIPTION_SOCKETS.lock();
|
||||
for path in (*sockets).values() {
|
||||
if let Ok(stream) = UnixStream::connect(path) {
|
||||
stream.shutdown(Shutdown::Both)?;
|
||||
}
|
||||
}
|
||||
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
let _ = std::fs::remove_file(socket);
|
||||
|
||||
std::process::exit(130);
|
||||
}
|
||||
|
||||
@@ -12,11 +12,15 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::Rect;
|
||||
use crate::core::Rect;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Layout;
|
||||
use crate::OperationDirection;
|
||||
use crate::WindowsApi;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -87,6 +91,22 @@ pub fn new(
|
||||
}
|
||||
|
||||
impl Monitor {
|
||||
pub fn placeholder() -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
name: "PLACEHOLDER".to_string(),
|
||||
device: "".to_string(),
|
||||
device_id: "".to_string(),
|
||||
size: Default::default(),
|
||||
work_area_size: Default::default(),
|
||||
work_area_offset: None,
|
||||
window_based_work_area_offset: None,
|
||||
window_based_work_area_offset_limit: 0,
|
||||
workspaces: Default::default(),
|
||||
last_focused_workspace: None,
|
||||
workspace_names: Default::default(),
|
||||
}
|
||||
}
|
||||
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
||||
let focused_idx = self.focused_workspace_idx();
|
||||
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
|
||||
@@ -114,7 +134,7 @@ impl Monitor {
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
};
|
||||
|
||||
workspace.add_container(container);
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -149,6 +169,7 @@ impl Monitor {
|
||||
&mut self,
|
||||
target_workspace_idx: usize,
|
||||
follow: bool,
|
||||
direction: Option<OperationDirection>,
|
||||
) -> Result<()> {
|
||||
let workspace = self
|
||||
.focused_workspace_mut()
|
||||
@@ -158,22 +179,92 @@ impl Monitor {
|
||||
bail!("cannot move native maximized window to another monitor or workspace");
|
||||
}
|
||||
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
let foreground_hwnd = WindowsApi::foreground_window()?;
|
||||
let floating_window_index = workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.position(|w| w.hwnd == foreground_hwnd);
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
if let Some(idx) = floating_window_index {
|
||||
let window = workspace.floating_windows_mut().remove(idx);
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
let workspaces = self.workspaces_mut();
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
target_workspace.floating_windows_mut().push(window);
|
||||
} else {
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
match direction {
|
||||
Some(OperationDirection::Left) => match target_workspace.layout() {
|
||||
Layout::Default(layout) => match layout {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
target_workspace.add_container_to_front(container);
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace.insert_container_at_idx(0, container);
|
||||
} else {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
},
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
},
|
||||
Some(OperationDirection::Right) => match target_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(target_workspace.containers().len());
|
||||
|
||||
match layout {
|
||||
DefaultLayout::RightMainVerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace.add_container_to_back(container);
|
||||
} else {
|
||||
target_workspace
|
||||
.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_front(container);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
target_workspace.add_container(container);
|
||||
}
|
||||
|
||||
if follow {
|
||||
self.focus_workspace(target_workspace_idx)?;
|
||||
|
||||
@@ -28,6 +28,7 @@ use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
|
||||
|
||||
use crate::monitor_reconciliator;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
|
||||
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
|
||||
@@ -44,7 +45,7 @@ impl From<isize> for Hidden {
|
||||
|
||||
impl Hidden {
|
||||
pub const fn hwnd(self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
}
|
||||
|
||||
pub fn create(name: &str) -> color_eyre::Result<Self> {
|
||||
@@ -65,8 +66,9 @@ impl Hidden {
|
||||
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
|
||||
let instance = h_module.0 as isize;
|
||||
std::thread::spawn(move || -> color_eyre::Result<()> {
|
||||
let hwnd = WindowsApi::create_hidden_window(PCWSTR(name.as_ptr()), h_module)?;
|
||||
let hwnd = WindowsApi::create_hidden_window(PCWSTR(name.as_ptr()), instance)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
|
||||
let mut msg: MSG = MSG::default();
|
||||
@@ -77,7 +79,8 @@ impl Hidden {
|
||||
tracing::debug!("hidden window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
TranslateMessage(&msg);
|
||||
// TODO: error handling
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::core::Rect;
|
||||
use crate::monitor;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::monitor_reconciliator::hidden::Hidden;
|
||||
@@ -10,7 +11,6 @@ use crate::WindowsApi;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::Rect;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@@ -67,11 +67,17 @@ pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
|
||||
.flatten()
|
||||
.map(|display| {
|
||||
let path = display.device_path;
|
||||
let mut split: Vec<_> = path.split('#').collect();
|
||||
split.remove(0);
|
||||
split.remove(split.len() - 1);
|
||||
let device = split[0].to_string();
|
||||
let device_id = split.join("-");
|
||||
|
||||
let (device, device_id) = if path.is_empty() {
|
||||
(String::from("UNKNOWN"), String::from("UNKNOWN"))
|
||||
} else {
|
||||
let mut split: Vec<_> = path.split('#').collect();
|
||||
split.remove(0);
|
||||
split.remove(split.len() - 1);
|
||||
let device = split[0].to_string();
|
||||
let device_id = split.join("-");
|
||||
(device, device_id)
|
||||
};
|
||||
|
||||
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
|
||||
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
|
||||
@@ -166,7 +172,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
if should_update {
|
||||
tracing::info!("updated work area for {}", monitor.device_id());
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"work areas match, reconciliation not required for {}",
|
||||
@@ -213,7 +219,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
);
|
||||
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"resolutions match, reconciliation not required for {}",
|
||||
@@ -313,7 +319,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
// Put the orphaned containers somewhere visible
|
||||
for container in orphaned_containers {
|
||||
focused_ws.add_container(container);
|
||||
focused_ws.add_container_to_back(container);
|
||||
}
|
||||
|
||||
// Gotta reset the focus or the movement will feel "off"
|
||||
@@ -400,7 +406,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Second retile to fix DPI/resolution related jank
|
||||
wm.retile_all(true)?;
|
||||
// Border updates to fix DPI/resolution related jank
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::fs::OpenOptions;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::net::Shutdown;
|
||||
use std::net::TcpListener;
|
||||
use std::net::TcpStream;
|
||||
use std::num::NonZeroUsize;
|
||||
@@ -21,29 +22,34 @@ use schemars::gen::SchemaSettings;
|
||||
use schemars::schema_for;
|
||||
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;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::MoveBehaviour;
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::Sizing;
|
||||
use komorebi_core::SocketMessage;
|
||||
use komorebi_core::StateQuery;
|
||||
use komorebi_core::WindowContainerBehaviour;
|
||||
use komorebi_core::WindowKind;
|
||||
use crate::core::config_generation::ApplicationConfiguration;
|
||||
use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use crate::core::ApplicationIdentifier;
|
||||
use crate::core::Axis;
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::FocusFollowsMouseImplementation;
|
||||
use crate::core::Layout;
|
||||
use crate::core::MoveBehaviour;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
use crate::core::Sizing;
|
||||
use crate::core::SocketMessage;
|
||||
use crate::core::StateQuery;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowKind;
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::IMPLEMENTATION;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::colour::Rgb;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::notify_subscribers;
|
||||
use crate::stackbar_manager;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::transparency_manager;
|
||||
use crate::window::RuleDebug;
|
||||
@@ -51,14 +57,20 @@ use crate::window::Window;
|
||||
use crate::window_manager;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener;
|
||||
use crate::GlobalState;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_ENABLED;
|
||||
use crate::ANIMATION_FPS;
|
||||
use crate::ANIMATION_STYLE;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
@@ -68,9 +80,11 @@ use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::SUBSCRIPTION_PIPES;
|
||||
use crate::SUBSCRIPTION_SOCKETS;
|
||||
use crate::SUBSCRIPTION_SOCKET_OPTIONS;
|
||||
use crate::TCP_CONNECTIONS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
use crate::WINDOWS_11;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
use stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use stackbar_manager::STACKBAR_LABEL;
|
||||
use stackbar_manager::STACKBAR_MODE;
|
||||
@@ -175,6 +189,10 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_asref)]
|
||||
// We don't have From implemented for &mut WindowManager
|
||||
let initial_state = State::from(self.as_ref());
|
||||
|
||||
match message {
|
||||
SocketMessage::CycleFocusWorkspace(_) | SocketMessage::FocusWorkspaceNumber(_) => {
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
@@ -219,9 +237,20 @@ impl WindowManager {
|
||||
self.cycle_container_window_in_direction(direction)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
SocketMessage::FocusStackWindow(idx) => {
|
||||
// In case you are using this command on a bar on a monitor
|
||||
// different from the currently focused one, you'd want that
|
||||
// monitor to be focused so that the FocusStackWindow happens
|
||||
// on the monitor with the bar you just pressed.
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
self.focus_container_window(idx)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
SocketMessage::ForceFocus => {
|
||||
let focused_window = self.focused_window()?;
|
||||
let focused_window_rect = WindowsApi::window_rect(focused_window.hwnd())?;
|
||||
let focused_window_rect = WindowsApi::window_rect(focused_window.hwnd)?;
|
||||
WindowsApi::center_cursor_in_rect(&focused_window_rect)?;
|
||||
WindowsApi::left_click();
|
||||
}
|
||||
@@ -254,26 +283,103 @@ impl WindowManager {
|
||||
self.set_workspace_padding(monitor_idx, workspace_idx, size)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::InitialWorkspaceRule(_, ref id, monitor_idx, workspace_idx) => {
|
||||
self.handle_initial_workspace_rules(id, monitor_idx, workspace_idx)?;
|
||||
SocketMessage::InitialWorkspaceRule(identifier, ref id, monitor_idx, workspace_idx) => {
|
||||
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
let workspace_matching_rule = WorkspaceMatchingRule {
|
||||
monitor_index: monitor_idx,
|
||||
workspace_index: workspace_idx,
|
||||
matching_rule: MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.to_string(),
|
||||
matching_strategy: Some(MatchingStrategy::Legacy),
|
||||
}),
|
||||
initial_only: true,
|
||||
};
|
||||
|
||||
if !workspace_rules.contains(&workspace_matching_rule) {
|
||||
workspace_rules.push(workspace_matching_rule);
|
||||
}
|
||||
}
|
||||
SocketMessage::InitialNamedWorkspaceRule(_, ref id, ref workspace) => {
|
||||
SocketMessage::InitialNamedWorkspaceRule(identifier, ref id, ref workspace) => {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
self.monitor_workspace_index_by_name(workspace)
|
||||
{
|
||||
self.handle_initial_workspace_rules(id, monitor_idx, workspace_idx)?;
|
||||
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
let workspace_matching_rule = WorkspaceMatchingRule {
|
||||
monitor_index: monitor_idx,
|
||||
workspace_index: workspace_idx,
|
||||
matching_rule: MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.to_string(),
|
||||
matching_strategy: Some(MatchingStrategy::Legacy),
|
||||
}),
|
||||
initial_only: true,
|
||||
};
|
||||
|
||||
if !workspace_rules.contains(&workspace_matching_rule) {
|
||||
workspace_rules.push(workspace_matching_rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::WorkspaceRule(_, ref id, monitor_idx, workspace_idx) => {
|
||||
self.handle_definitive_workspace_rules(id, monitor_idx, workspace_idx)?;
|
||||
SocketMessage::WorkspaceRule(identifier, ref id, monitor_idx, workspace_idx) => {
|
||||
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
let workspace_matching_rule = WorkspaceMatchingRule {
|
||||
monitor_index: monitor_idx,
|
||||
workspace_index: workspace_idx,
|
||||
matching_rule: MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.to_string(),
|
||||
matching_strategy: Some(MatchingStrategy::Legacy),
|
||||
}),
|
||||
initial_only: false,
|
||||
};
|
||||
|
||||
if !workspace_rules.contains(&workspace_matching_rule) {
|
||||
workspace_rules.push(workspace_matching_rule);
|
||||
}
|
||||
}
|
||||
SocketMessage::NamedWorkspaceRule(_, ref id, ref workspace) => {
|
||||
SocketMessage::NamedWorkspaceRule(identifier, ref id, ref workspace) => {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
self.monitor_workspace_index_by_name(workspace)
|
||||
{
|
||||
self.handle_definitive_workspace_rules(id, monitor_idx, workspace_idx)?;
|
||||
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
let workspace_matching_rule = WorkspaceMatchingRule {
|
||||
monitor_index: monitor_idx,
|
||||
workspace_index: workspace_idx,
|
||||
matching_rule: MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.to_string(),
|
||||
matching_strategy: Some(MatchingStrategy::Legacy),
|
||||
}),
|
||||
initial_only: false,
|
||||
};
|
||||
|
||||
if !workspace_rules.contains(&workspace_matching_rule) {
|
||||
workspace_rules.push(workspace_matching_rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::ClearWorkspaceRules(monitor_idx, workspace_idx) => {
|
||||
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
|
||||
workspace_rules.retain(|r| {
|
||||
r.monitor_index != monitor_idx && r.workspace_index != workspace_idx
|
||||
});
|
||||
}
|
||||
SocketMessage::ClearNamedWorkspaceRules(ref workspace) => {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
self.monitor_workspace_index_by_name(workspace)
|
||||
{
|
||||
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
workspace_rules.retain(|r| {
|
||||
r.monitor_index != monitor_idx && r.workspace_index != workspace_idx
|
||||
});
|
||||
}
|
||||
}
|
||||
SocketMessage::ClearAllWorkspaceRules => {
|
||||
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
workspace_rules.clear();
|
||||
}
|
||||
SocketMessage::ManageRule(identifier, ref id) => {
|
||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
|
||||
@@ -294,20 +400,20 @@ impl WindowManager {
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::FloatRule(identifier, ref id) => {
|
||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
SocketMessage::IgnoreRule(identifier, ref id) => {
|
||||
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
|
||||
|
||||
let mut should_push = true;
|
||||
for f in &*float_identifiers {
|
||||
if let MatchingRule::Simple(f) = f {
|
||||
if f.id.eq(id) {
|
||||
for i in &*ignore_identifiers {
|
||||
if let MatchingRule::Simple(i) = i {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
float_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
ignore_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
@@ -393,7 +499,7 @@ impl WindowManager {
|
||||
self.adjust_workspace_padding(sizing, adjustment)?;
|
||||
}
|
||||
SocketMessage::MoveContainerToWorkspaceNumber(workspace_idx) => {
|
||||
self.move_container_to_workspace(workspace_idx, true)?;
|
||||
self.move_container_to_workspace(workspace_idx, true, None)?;
|
||||
}
|
||||
SocketMessage::CycleMoveContainerToWorkspace(direction) => {
|
||||
let focused_monitor = self
|
||||
@@ -409,7 +515,7 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
|
||||
);
|
||||
|
||||
self.move_container_to_workspace(workspace_idx, true)?;
|
||||
self.move_container_to_workspace(workspace_idx, true, None)?;
|
||||
}
|
||||
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, None, true)?;
|
||||
@@ -427,7 +533,7 @@ impl WindowManager {
|
||||
self.move_container_to_monitor(monitor_idx, None, true)?;
|
||||
}
|
||||
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
|
||||
self.move_container_to_workspace(workspace_idx, false)?;
|
||||
self.move_container_to_workspace(workspace_idx, false, None)?;
|
||||
}
|
||||
SocketMessage::CycleSendContainerToWorkspace(direction) => {
|
||||
let focused_monitor = self
|
||||
@@ -443,7 +549,7 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
|
||||
);
|
||||
|
||||
self.move_container_to_workspace(workspace_idx, false)?;
|
||||
self.move_container_to_workspace(workspace_idx, false, None)?;
|
||||
}
|
||||
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, None, false)?;
|
||||
@@ -522,6 +628,7 @@ impl WindowManager {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
||||
}
|
||||
SocketMessage::Retile => {
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
border_manager::destroy_all_borders()?;
|
||||
self.retile_all(false)?
|
||||
}
|
||||
@@ -731,6 +838,16 @@ impl WindowManager {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
}
|
||||
|
||||
let sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
for path in (*sockets).values() {
|
||||
if let Ok(stream) = UnixStream::connect(path) {
|
||||
stream.shutdown(Shutdown::Both)?;
|
||||
}
|
||||
}
|
||||
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
let _ = std::fs::remove_file(socket);
|
||||
|
||||
std::process::exit(0)
|
||||
}
|
||||
SocketMessage::MonitorIndexPreference(index_preference, left, top, right, bottom) => {
|
||||
@@ -1038,6 +1155,33 @@ impl WindowManager {
|
||||
SocketMessage::ReloadConfiguration => {
|
||||
Self::reload_configuration();
|
||||
}
|
||||
SocketMessage::ReplaceConfiguration(ref config) => {
|
||||
// Check that this is a valid static config file first
|
||||
if StaticConfig::read(config).is_ok() {
|
||||
// Clear workspace rules; these will need to be replaced
|
||||
WORKSPACE_MATCHING_RULES.lock().clear();
|
||||
// Pause so that restored windows come to the foreground from all workspaces
|
||||
self.is_paused = true;
|
||||
// Bring all windows to the foreground
|
||||
self.restore_all_windows()?;
|
||||
|
||||
// Create a new wm from the config path
|
||||
let mut wm = StaticConfig::preload(
|
||||
config,
|
||||
winevent_listener::event_rx(),
|
||||
self.command_listener.try_clone().ok(),
|
||||
)?;
|
||||
|
||||
// Initialize the new wm
|
||||
wm.init()?;
|
||||
|
||||
// This is equivalent to StaticConfig::postload for this use case
|
||||
StaticConfig::reload(config, &mut wm)?;
|
||||
|
||||
// Set self to the new wm instance
|
||||
*self = wm;
|
||||
}
|
||||
}
|
||||
SocketMessage::ReloadStaticConfiguration(ref pathbuf) => {
|
||||
self.reload_static_configuration(pathbuf)?;
|
||||
}
|
||||
@@ -1181,6 +1325,14 @@ impl WindowManager {
|
||||
let socket_path = DATA_DIR.join(socket);
|
||||
sockets.insert(socket.clone(), socket_path);
|
||||
}
|
||||
SocketMessage::AddSubscriberSocketWithOptions(ref socket, options) => {
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
let socket_path = DATA_DIR.join(socket);
|
||||
sockets.insert(socket.clone(), socket_path);
|
||||
|
||||
let mut socket_options = SUBSCRIPTION_SOCKET_OPTIONS.lock();
|
||||
socket_options.insert(socket.clone(), options);
|
||||
}
|
||||
SocketMessage::RemoveSubscriberSocket(ref socket) => {
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
sockets.remove(socket);
|
||||
@@ -1208,15 +1360,52 @@ impl WindowManager {
|
||||
self.resize_delta = delta;
|
||||
}
|
||||
SocketMessage::ToggleWindowContainerBehaviour => {
|
||||
match self.window_container_behaviour {
|
||||
match self.window_management_behaviour.current_behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
self.window_container_behaviour = WindowContainerBehaviour::Append;
|
||||
self.window_management_behaviour.current_behaviour =
|
||||
WindowContainerBehaviour::Append;
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
self.window_container_behaviour = WindowContainerBehaviour::Create;
|
||||
self.window_management_behaviour.current_behaviour =
|
||||
WindowContainerBehaviour::Create;
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleFloatOverride => {
|
||||
self.window_management_behaviour.float_override =
|
||||
!self.window_management_behaviour.float_override;
|
||||
}
|
||||
SocketMessage::ToggleWorkspaceWindowContainerBehaviour => {
|
||||
let current_global_behaviour = self.window_management_behaviour.current_behaviour;
|
||||
if let Some(behaviour) = self
|
||||
.focused_workspace_mut()?
|
||||
.window_container_behaviour_mut()
|
||||
{
|
||||
match behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
*behaviour = WindowContainerBehaviour::Append
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
*behaviour = WindowContainerBehaviour::Create
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.focused_workspace_mut()?
|
||||
.set_window_container_behaviour(Some(match current_global_behaviour {
|
||||
WindowContainerBehaviour::Create => WindowContainerBehaviour::Append,
|
||||
WindowContainerBehaviour::Append => WindowContainerBehaviour::Create,
|
||||
}));
|
||||
};
|
||||
}
|
||||
SocketMessage::ToggleWorkspaceFloatOverride => {
|
||||
let current_global_override = self.window_management_behaviour.float_override;
|
||||
if let Some(float_override) = self.focused_workspace_mut()?.float_override_mut() {
|
||||
*float_override = !*float_override;
|
||||
} else {
|
||||
self.focused_workspace_mut()?
|
||||
.set_float_override(Some(!current_global_override));
|
||||
};
|
||||
}
|
||||
SocketMessage::WindowHidingBehaviour(behaviour) => {
|
||||
let mut hiding_behaviour = HIDING_BEHAVIOUR.lock();
|
||||
*hiding_behaviour = behaviour;
|
||||
@@ -1241,6 +1430,25 @@ impl WindowManager {
|
||||
SocketMessage::Border(enable) => {
|
||||
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::BorderImplementation(implementation) => {
|
||||
if !*WINDOWS_11 && matches!(implementation, BorderImplementation::Windows) {
|
||||
tracing::error!(
|
||||
"BorderImplementation::Windows is only supported on Windows 11 and above"
|
||||
);
|
||||
} else {
|
||||
IMPLEMENTATION.store(implementation);
|
||||
match IMPLEMENTATION.load() {
|
||||
BorderImplementation::Komorebi => {
|
||||
self.remove_all_accents()?;
|
||||
}
|
||||
BorderImplementation::Windows => {
|
||||
border_manager::destroy_all_borders()?;
|
||||
}
|
||||
}
|
||||
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
}
|
||||
SocketMessage::BorderColour(kind, r, g, b) => match kind {
|
||||
WindowKind::Single => {
|
||||
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
@@ -1254,6 +1462,9 @@ impl WindowManager {
|
||||
WindowKind::Unfocused => {
|
||||
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Floating => {
|
||||
border_manager::FLOATING.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
},
|
||||
SocketMessage::BorderStyle(style) => {
|
||||
STYLE.store(style);
|
||||
@@ -1264,6 +1475,22 @@ impl WindowManager {
|
||||
SocketMessage::BorderOffset(offset) => {
|
||||
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::Animation(enable) => {
|
||||
ANIMATION_ENABLED.store(enable, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::AnimationDuration(duration) => {
|
||||
ANIMATION_DURATION.store(duration, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::AnimationFps(fps) => {
|
||||
ANIMATION_FPS.store(fps, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::AnimationStyle(style) => {
|
||||
*ANIMATION_STYLE.lock() = style;
|
||||
}
|
||||
SocketMessage::ToggleTransparency => {
|
||||
let current = transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst);
|
||||
transparency_manager::TRANSPARENCY_ENABLED.store(!current, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::Transparency(enable) => {
|
||||
transparency_manager::TRANSPARENCY_ENABLED.store(enable, Ordering::SeqCst);
|
||||
}
|
||||
@@ -1294,6 +1521,13 @@ impl WindowManager {
|
||||
SocketMessage::StackbarTabWidth(width) => {
|
||||
STACKBAR_TAB_WIDTH.store(width, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::StackbarFontSize(size) => {
|
||||
STACKBAR_FONT_SIZE.store(size, Ordering::SeqCst);
|
||||
}
|
||||
#[allow(clippy::assigning_clones)]
|
||||
SocketMessage::StackbarFontFamily(ref font_family) => {
|
||||
*STACKBAR_FONT_FAMILY.lock() = font_family.clone();
|
||||
}
|
||||
SocketMessage::ApplicationSpecificConfigurationSchema => {
|
||||
let asc = schema_for!(Vec<ApplicationConfiguration>);
|
||||
let schema = serde_json::to_string_pretty(&asc)?;
|
||||
@@ -1354,64 +1588,21 @@ impl WindowManager {
|
||||
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
|
||||
};
|
||||
|
||||
let notification = Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: self.as_ref().into(),
|
||||
};
|
||||
notify_subscribers(
|
||||
Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: self.as_ref().into(),
|
||||
},
|
||||
initial_state.has_been_modified(self.as_ref()),
|
||||
)?;
|
||||
|
||||
notify_subscribers(&serde_json::to_string(¬ification)?)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
transparency_manager::send_notification();
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
tracing::info!("processed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
fn handle_initial_workspace_rules(
|
||||
&mut self,
|
||||
id: &String,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
) -> Result<()> {
|
||||
self.handle_workspace_rules(id, monitor_idx, workspace_idx, true)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
fn handle_definitive_workspace_rules(
|
||||
&mut self,
|
||||
id: &String,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
) -> Result<()> {
|
||||
self.handle_workspace_rules(id, monitor_idx, workspace_idx, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
pub fn handle_workspace_rules(
|
||||
&mut self,
|
||||
id: &String,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
initial_workspace_rule: bool,
|
||||
) -> Result<()> {
|
||||
{
|
||||
let mut workspace_rules = WORKSPACE_RULES.lock();
|
||||
workspace_rules.insert(
|
||||
id.to_string(),
|
||||
(monitor_idx, workspace_idx, initial_workspace_rule),
|
||||
);
|
||||
}
|
||||
|
||||
self.enforce_workspace_rules()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream) -> Result<()> {
|
||||
|
||||
@@ -9,10 +9,10 @@ use color_eyre::Result;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::Sizing;
|
||||
use komorebi_core::WindowContainerBehaviour;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
use crate::core::Sizing;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
@@ -32,7 +32,9 @@ use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::DATA_DIR;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
@@ -63,7 +65,7 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
|
||||
|
||||
impl WindowManager {
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[tracing::instrument(skip(self, event), fields(event = event.title(), winevent = event.winevent(), hwnd = event.hwnd()))]
|
||||
pub fn process_event(&mut self, event: WindowManagerEvent) -> Result<()> {
|
||||
if self.is_paused {
|
||||
tracing::trace!("ignoring while paused");
|
||||
@@ -101,6 +103,10 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
if !transparency_override {
|
||||
if rule_debug.matches_ignore_identifier.is_some() {
|
||||
border_manager::send_notification(Option::from(event.hwnd()));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -117,6 +123,10 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_asref)]
|
||||
// We don't have From implemented for &mut WindowManager
|
||||
let initial_state = State::from(self.as_ref());
|
||||
|
||||
// Make sure we have the most recently focused monitor from any event
|
||||
match event {
|
||||
WindowManagerEvent::FocusChange(_, window)
|
||||
@@ -149,14 +159,6 @@ impl WindowManager {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
for workspace in monitor.workspaces_mut() {
|
||||
if let WindowManagerEvent::FocusChange(_, window) = event {
|
||||
let _ = workspace.focus_changed(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.enforce_workspace_rules()?;
|
||||
|
||||
if matches!(event, WindowManagerEvent::MouseCapture(..)) {
|
||||
@@ -246,24 +248,31 @@ impl WindowManager {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if !workspace
|
||||
let floating_window_idx = workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.any(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
if let Some(w) = workspace.maximized_window() {
|
||||
if w.hwnd == window.hwnd {
|
||||
return Ok(());
|
||||
.position(|w| w.hwnd == window.hwnd);
|
||||
|
||||
match floating_window_idx {
|
||||
None => {
|
||||
if let Some(w) = workspace.maximized_window() {
|
||||
if w.hwnd == window.hwnd {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
window.focus(false)?;
|
||||
}
|
||||
} else {
|
||||
workspace.focus_container_by_window(window.hwnd)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
Some(idx) => {
|
||||
if let Some(window) = workspace.floating_windows().get(idx) {
|
||||
window.focus(false)?;
|
||||
}
|
||||
} else {
|
||||
self.focused_workspace_mut()?
|
||||
.focus_container_by_window(window.hwnd)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,23 +338,53 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
if proceed {
|
||||
let behaviour = self.window_container_behaviour;
|
||||
let mut behaviour = self
|
||||
.window_management_behaviour(focused_monitor_idx, focused_workspace_idx);
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let workspace_contains_window = workspace.contains_window(window.hwnd);
|
||||
let monocle_container = workspace.monocle_container().clone();
|
||||
|
||||
if !workspace_contains_window && !needs_reconciliation {
|
||||
match behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
workspace.new_container_for_window(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
let floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
let mut should_float = false;
|
||||
|
||||
if !floating_applications.is_empty() {
|
||||
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) =
|
||||
(window.title(), window.exe(), window.class(), window.path())
|
||||
{
|
||||
should_float = should_act(
|
||||
&title,
|
||||
&exe_name,
|
||||
&class,
|
||||
&path,
|
||||
&floating_applications,
|
||||
®ex_identifiers,
|
||||
)
|
||||
.is_some();
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
workspace
|
||||
.focused_container_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused container"))?
|
||||
.add_window(window);
|
||||
self.update_focused_workspace(true, false)?;
|
||||
}
|
||||
|
||||
behaviour.float_override = behaviour.float_override
|
||||
|| (should_float && !matches!(event, WindowManagerEvent::Manage(_)));
|
||||
|
||||
if behaviour.float_override {
|
||||
workspace.floating_windows_mut().push(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
} else {
|
||||
match behaviour.current_behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
workspace.new_container_for_window(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
workspace
|
||||
.focused_container_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused container"))?
|
||||
.add_window(window);
|
||||
self.update_focused_workspace(true, false)?;
|
||||
stackbar_manager::send_notification();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -380,7 +419,7 @@ impl WindowManager {
|
||||
.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));
|
||||
@@ -397,12 +436,14 @@ impl WindowManager {
|
||||
.monitor_idx_from_current_pos()
|
||||
.ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?;
|
||||
|
||||
let new_window_behaviour = self.window_container_behaviour;
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
|
||||
let window_management_behaviour =
|
||||
self.window_management_behaviour(focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
let new_position = WindowsApi::window_rect(window.hwnd())?;
|
||||
let new_position = WindowsApi::window_rect(window.hwnd)?;
|
||||
let old_position = *workspace
|
||||
.latest_layout()
|
||||
.get(focused_container_idx)
|
||||
@@ -516,8 +557,11 @@ impl WindowManager {
|
||||
}
|
||||
// Here we handle a simple move on the same monitor which is treated as
|
||||
// a container swap
|
||||
} else if window_management_behaviour.float_override {
|
||||
workspace.floating_windows_mut().push(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
} else {
|
||||
match new_window_behaviour {
|
||||
match window_management_behaviour.current_behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
match workspace.container_idx_from_current_point() {
|
||||
Some(target_idx) => {
|
||||
@@ -546,6 +590,8 @@ impl WindowManager {
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
stackbar_manager::send_notification();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,14 +620,19 @@ impl WindowManager {
|
||||
ops.push(resize_op!(resize.top, >, OperationDirection::Up));
|
||||
}
|
||||
|
||||
// TODO: Determine if this is still needed
|
||||
let top_left_constant = BORDER_WIDTH.load(Ordering::SeqCst)
|
||||
+ BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
|
||||
if resize.right != 0 && resize.left == top_left_constant {
|
||||
if resize.right != 0
|
||||
&& (resize.left == top_left_constant || resize.left == 0)
|
||||
{
|
||||
ops.push(resize_op!(resize.right, <, OperationDirection::Right));
|
||||
}
|
||||
|
||||
if resize.bottom != 0 && resize.top == top_left_constant {
|
||||
if resize.bottom != 0
|
||||
&& (resize.top == top_left_constant || resize.top == 0)
|
||||
{
|
||||
ops.push(resize_op!(resize.bottom, <, OperationDirection::Down));
|
||||
}
|
||||
|
||||
@@ -599,7 +650,7 @@ impl WindowManager {
|
||||
};
|
||||
|
||||
// If we unmanaged a window, it shouldn't be immediately hidden behind managed windows
|
||||
if let WindowManagerEvent::Unmanage(window) = event {
|
||||
if let WindowManagerEvent::Unmanage(mut window) = event {
|
||||
window.center(&self.focused_monitor_work_area()?)?;
|
||||
}
|
||||
|
||||
@@ -624,13 +675,15 @@ impl WindowManager {
|
||||
|
||||
serde_json::to_writer_pretty(&file, &known_hwnds)?;
|
||||
|
||||
let notification = Notification {
|
||||
event: NotificationEvent::WindowManager(event),
|
||||
state: self.as_ref().into(),
|
||||
};
|
||||
notify_subscribers(
|
||||
Notification {
|
||||
event: NotificationEvent::WindowManager(event),
|
||||
state: self.as_ref().into(),
|
||||
},
|
||||
initial_state.has_been_modified(self.as_ref()),
|
||||
)?;
|
||||
|
||||
notify_subscribers(&serde_json::to_string(¬ification)?)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(Some(event.hwnd()));
|
||||
transparency_manager::send_notification();
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use winput::message_loop;
|
||||
use winput::message_loop::Event;
|
||||
use winput::Action;
|
||||
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use crate::core::FocusFollowsMouseImplementation;
|
||||
|
||||
use crate::window_manager::WindowManager;
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
let reaped_orphans = workspace.reap_orphans()?;
|
||||
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
|
||||
workspace.update(&work_area, offset, window_based_work_area_offset)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
tracing::info!(
|
||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||
reaped_orphans.0,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod stackbar;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::core::StackbarLabel;
|
||||
use crate::core::StackbarMode;
|
||||
use crate::stackbar_manager::stackbar::Stackbar;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
@@ -9,18 +11,18 @@ use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::StackbarLabel;
|
||||
use komorebi_core::StackbarMode;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
pub static STACKBAR_FONT_SIZE: AtomicI32 = AtomicI32::new(0); // 0 will produce the system default
|
||||
pub static STACKBAR_FOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(16777215); // white
|
||||
pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947); // gray text
|
||||
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
|
||||
@@ -29,8 +31,11 @@ pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
|
||||
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
|
||||
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::OnStack);
|
||||
|
||||
pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref STACKBAR_STATE: Mutex<HashMap<String, Stackbar>> = Mutex::new(HashMap::new());
|
||||
pub static ref STACKBAR_FONT_FAMILY: Mutex<Option<String>> = Mutex::new(None);
|
||||
static ref STACKBARS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
|
||||
static ref STACKBARS_CONTAINERS: Mutex<HashMap<isize, Container>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
@@ -91,7 +96,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let mut state = wm.lock();
|
||||
|
||||
// If stackbars are disabled
|
||||
if matches!(STACKBAR_MODE.load(), StackbarMode::Never) {
|
||||
if matches!(STACKBAR_MODE.load(), StackbarMode::Never)
|
||||
|| STACKBAR_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
|
||||
{
|
||||
for (_, stackbar) in stackbars.iter() {
|
||||
stackbar.destroy()?;
|
||||
}
|
||||
@@ -120,9 +127,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
let is_maximized = WindowsApi::is_zoomed(HWND(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
));
|
||||
let is_maximized =
|
||||
WindowsApi::is_zoomed(WindowsApi::foreground_window().unwrap_or_default());
|
||||
|
||||
// Handle the monocle container separately
|
||||
if ws.monocle_container().is_some() || is_maximized {
|
||||
@@ -199,11 +205,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
stackbars_monitors.insert(container.id().clone(), monitor_idx);
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
container
|
||||
.focused_window()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.hwnd(),
|
||||
container.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
stackbar.update(container_padding, container, &rect)?;
|
||||
|
||||
@@ -2,24 +2,30 @@ use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::container::Container;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
use crate::core::StackbarLabel;
|
||||
use crate::stackbar_manager::STACKBARS_CONTAINERS;
|
||||
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
|
||||
use crate::stackbar_manager::STACKBAR_LABEL;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
|
||||
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::WINDOWS_11;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::BorderStyle;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::StackbarLabel;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::HINSTANCE;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
@@ -30,6 +36,7 @@ use windows::Win32::Graphics::Gdi::CreateSolidBrush;
|
||||
use windows::Win32::Graphics::Gdi::DeleteObject;
|
||||
use windows::Win32::Graphics::Gdi::DrawTextW;
|
||||
use windows::Win32::Graphics::Gdi::GetDC;
|
||||
use windows::Win32::Graphics::Gdi::GetDeviceCaps;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::ReleaseDC;
|
||||
use windows::Win32::Graphics::Gdi::RoundRect;
|
||||
@@ -43,8 +50,10 @@ use windows::Win32::Graphics::Gdi::DT_VCENTER;
|
||||
use windows::Win32::Graphics::Gdi::FONT_QUALITY;
|
||||
use windows::Win32::Graphics::Gdi::FW_BOLD;
|
||||
use windows::Win32::Graphics::Gdi::LOGFONTW;
|
||||
use windows::Win32::Graphics::Gdi::LOGPIXELSY;
|
||||
use windows::Win32::Graphics::Gdi::PROOF_QUALITY;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
use windows::Win32::System::WindowsProgramming::MulDiv;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
@@ -77,7 +86,7 @@ impl From<isize> for Stackbar {
|
||||
|
||||
impl Stackbar {
|
||||
pub const fn hwnd(&self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
}
|
||||
|
||||
pub fn create(id: &str) -> color_eyre::Result<Self> {
|
||||
@@ -100,6 +109,7 @@ impl Stackbar {
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
|
||||
let name_cl = name.clone();
|
||||
let instance = h_module.0 as isize;
|
||||
std::thread::spawn(move || -> color_eyre::Result<()> {
|
||||
unsafe {
|
||||
let hwnd = CreateWindowExW(
|
||||
@@ -113,12 +123,12 @@ impl Stackbar {
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
h_module,
|
||||
HINSTANCE(windows_api::as_ptr!(instance)),
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
hwnd_sender.send(hwnd.0 as isize)?;
|
||||
|
||||
let mut msg: MSG = MSG::default();
|
||||
|
||||
@@ -127,7 +137,8 @@ impl Stackbar {
|
||||
tracing::debug!("stackbar window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
TranslateMessage(&msg);
|
||||
// TODO: error handling
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
|
||||
std::thread::sleep(Duration::from_millis(10))
|
||||
@@ -138,12 +149,12 @@ impl Stackbar {
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
hwnd: hwnd_receiver.recv()?.0,
|
||||
hwnd: hwnd_receiver.recv()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
WindowsApi::close_window(self.hwnd())
|
||||
WindowsApi::close_window(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
@@ -169,7 +180,7 @@ impl Stackbar {
|
||||
layout.top -= workspace_specific_offset + STACKBAR_TAB_HEIGHT.load_consume();
|
||||
layout.left -= workspace_specific_offset;
|
||||
|
||||
WindowsApi::position_window(self.hwnd(), &layout, false)?;
|
||||
WindowsApi::position_window(self.hwnd, &layout, false)?;
|
||||
|
||||
unsafe {
|
||||
let hdc = GetDC(self.hwnd());
|
||||
@@ -181,11 +192,29 @@ impl Stackbar {
|
||||
SelectObject(hdc, hbrush);
|
||||
SetBkColor(hdc, COLORREF(background));
|
||||
|
||||
let hfont = CreateFontIndirectW(&LOGFONTW {
|
||||
let mut logfont = LOGFONTW {
|
||||
lfWeight: FW_BOLD.0 as i32,
|
||||
lfQuality: FONT_QUALITY(PROOF_QUALITY.0),
|
||||
lfFaceName: [0; 32],
|
||||
..Default::default()
|
||||
});
|
||||
};
|
||||
|
||||
if let Some(font_name) = &*STACKBAR_FONT_FAMILY.lock() {
|
||||
let font = wide_string(font_name);
|
||||
for (i, &c) in font.iter().enumerate() {
|
||||
logfont.lfFaceName[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
let logical_height = -MulDiv(
|
||||
STACKBAR_FONT_SIZE.load(Ordering::SeqCst),
|
||||
72,
|
||||
GetDeviceCaps(hdc, LOGPIXELSY),
|
||||
);
|
||||
|
||||
logfont.lfHeight = logical_height;
|
||||
|
||||
let hfont = CreateFontIndirectW(&logfont);
|
||||
|
||||
SelectObject(hdc, hfont);
|
||||
|
||||
@@ -207,16 +236,29 @@ impl Stackbar {
|
||||
match STYLE.load() {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
|
||||
// TODO: error handling
|
||||
let _ = RoundRect(
|
||||
hdc,
|
||||
rect.left,
|
||||
rect.top,
|
||||
rect.right,
|
||||
rect.bottom,
|
||||
20,
|
||||
20,
|
||||
);
|
||||
} else {
|
||||
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
|
||||
// TODO: error handling
|
||||
let _ = Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => {
|
||||
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
|
||||
// TODO: error handling
|
||||
let _ =
|
||||
RoundRect(hdc, rect.left, rect.top, rect.right, rect.bottom, 20, 20);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
|
||||
// TODO: error handling
|
||||
let _ = Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,9 +284,12 @@ impl Stackbar {
|
||||
}
|
||||
|
||||
ReleaseDC(self.hwnd(), hdc);
|
||||
DeleteObject(hpen);
|
||||
DeleteObject(hbrush);
|
||||
DeleteObject(hfont);
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hpen);
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hbrush);
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hfont);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -267,7 +312,7 @@ impl Stackbar {
|
||||
match msg {
|
||||
WM_LBUTTONDOWN => {
|
||||
let stackbars_containers = STACKBARS_CONTAINERS.lock();
|
||||
if let Some(container) = stackbars_containers.get(&hwnd.0) {
|
||||
if let Some(container) = stackbars_containers.get(&(hwnd.0 as isize)) {
|
||||
let x = l_param.0 as i32 & 0xFFFF;
|
||||
let y = (l_param.0 as i32 >> 16) & 0xFFFF;
|
||||
|
||||
@@ -277,11 +322,7 @@ impl Stackbar {
|
||||
|
||||
let focused_window_idx = container.focused_window_idx();
|
||||
let focused_window_rect = WindowsApi::window_rect(
|
||||
container
|
||||
.focused_window()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.hwnd(),
|
||||
container.focused_window().cloned().unwrap_or_default().hwnd,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -336,3 +377,10 @@ impl Stackbar {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wide_string(s: &str) -> Vec<u16> {
|
||||
std::ffi::OsStr::new(s)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,13 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicU8;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
use crate::should_act;
|
||||
use crate::Window;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::TRANSPARENCY_BLACKLIST;
|
||||
|
||||
pub static TRANSPARENCY_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
pub static TRANSPARENCY_ALPHA: AtomicU8 = AtomicU8::new(200);
|
||||
@@ -104,9 +106,18 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Monocle container is never transparent
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
if let Err(error) = window.opaque() {
|
||||
if monitor_idx == focused_monitor_idx {
|
||||
if let Err(error) = window.opaque() {
|
||||
let hwnd = window.hwnd;
|
||||
tracing::error!(
|
||||
"failed to make monocle window {hwnd} opaque: {error}"
|
||||
)
|
||||
}
|
||||
} else if let Err(error) = window.transparent() {
|
||||
let hwnd = window.hwnd;
|
||||
tracing::error!("failed to make monocle window {hwnd} opaque: {error}")
|
||||
tracing::error!(
|
||||
"failed to make monocle window {hwnd} transparent: {error}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +125,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let is_maximized = WindowsApi::is_zoomed(HWND(foreground_hwnd));
|
||||
let is_maximized = WindowsApi::is_zoomed(foreground_hwnd);
|
||||
|
||||
if is_maximized {
|
||||
if let Err(error) = Window::from(foreground_hwnd).opaque() {
|
||||
@@ -125,6 +136,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let transparency_blacklist = TRANSPARENCY_BLACKLIST.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
// Update the transparency for all containers on this workspace
|
||||
|
||||
@@ -135,15 +149,37 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let focused_window_idx = c.focused_window_idx();
|
||||
for (window_idx, window) in c.windows().iter().enumerate() {
|
||||
if window_idx == focused_window_idx {
|
||||
match window.transparent() {
|
||||
Err(error) => {
|
||||
let hwnd = foreground_hwnd;
|
||||
tracing::error!(
|
||||
"failed to make unfocused window {hwnd} transparent: {error}"
|
||||
)
|
||||
let mut should_make_transparent = true;
|
||||
if !transparency_blacklist.is_empty() {
|
||||
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) = (
|
||||
window.title(),
|
||||
window.exe(),
|
||||
window.class(),
|
||||
window.path(),
|
||||
) {
|
||||
let is_blacklisted = should_act(
|
||||
&title,
|
||||
&exe_name,
|
||||
&class,
|
||||
&path,
|
||||
&transparency_blacklist,
|
||||
®ex_identifiers,
|
||||
)
|
||||
.is_some();
|
||||
|
||||
should_make_transparent = !is_blacklisted;
|
||||
}
|
||||
Ok(..) => {
|
||||
known_hwnds.lock().push(window.hwnd);
|
||||
}
|
||||
|
||||
if should_make_transparent {
|
||||
match window.transparent() {
|
||||
Err(error) => {
|
||||
let hwnd = foreground_hwnd;
|
||||
tracing::error!("failed to make unfocused window {hwnd} transparent: {error}" )
|
||||
}
|
||||
Ok(..) => {
|
||||
known_hwnds.lock().push(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
use crate::border_manager;
|
||||
use crate::com::SetCloak;
|
||||
use crate::focus_manager;
|
||||
use crate::stackbar_manager;
|
||||
use crate::windows_api;
|
||||
use crate::ANIMATIONS_IN_PROGRESS;
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_ENABLED;
|
||||
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
|
||||
use crate::SLOW_APPLICATION_IDENTIFIERS;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::ser::SerializeStruct;
|
||||
@@ -20,18 +31,19 @@ use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
use crate::core::ApplicationIdentifier;
|
||||
use crate::core::HidingBehaviour;
|
||||
use crate::core::Rect;
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::styles::ExtendedWindowStyle;
|
||||
use crate::styles::WindowStyle;
|
||||
use crate::transparency_manager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
@@ -39,20 +51,31 @@ use crate::PERMAIGNORE_CLASSES;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::WSL2_UI_PROCESSES;
|
||||
|
||||
pub static MINIMUM_WIDTH: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Window {
|
||||
pub hwnd: isize,
|
||||
#[serde(skip)]
|
||||
animation: Animation,
|
||||
}
|
||||
|
||||
impl From<isize> for Window {
|
||||
fn from(value: isize) -> Self {
|
||||
Self { hwnd: value }
|
||||
Self {
|
||||
hwnd: value,
|
||||
animation: Animation::new(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HWND> for Window {
|
||||
fn from(value: HWND) -> Self {
|
||||
Self { hwnd: value.0 }
|
||||
Self {
|
||||
hwnd: value.0 as isize,
|
||||
animation: Animation::new(value.0 as isize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +148,7 @@ impl Serialize for Window {
|
||||
)?;
|
||||
state.serialize_field(
|
||||
"rect",
|
||||
&WindowsApi::window_rect(self.hwnd()).unwrap_or_default(),
|
||||
&WindowsApi::window_rect(self.hwnd).unwrap_or_default(),
|
||||
)?;
|
||||
state.end()
|
||||
}
|
||||
@@ -133,10 +156,35 @@ impl Serialize for Window {
|
||||
|
||||
impl Window {
|
||||
pub const fn hwnd(self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
}
|
||||
|
||||
pub fn center(&self, work_area: &Rect) -> Result<()> {
|
||||
pub fn move_to_area(&mut self, current_area: &Rect, target_area: &Rect) -> Result<()> {
|
||||
let current_rect = WindowsApi::window_rect(self.hwnd)?;
|
||||
let x_diff = target_area.left - current_area.left;
|
||||
let y_diff = target_area.top - current_area.top;
|
||||
let x_ratio = f32::abs((target_area.right as f32) / (current_area.right as f32));
|
||||
let y_ratio = f32::abs((target_area.bottom as f32) / (current_area.bottom as f32));
|
||||
let window_relative_x = current_rect.left - current_area.left;
|
||||
let window_relative_y = current_rect.top - current_area.top;
|
||||
let corrected_relative_x = (window_relative_x as f32 * x_ratio) as i32;
|
||||
let corrected_relative_y = (window_relative_y as f32 * y_ratio) as i32;
|
||||
let window_x = current_area.left + corrected_relative_x;
|
||||
let window_y = current_area.top + corrected_relative_y;
|
||||
|
||||
let new_rect = Rect {
|
||||
left: x_diff + window_x,
|
||||
top: y_diff + window_y,
|
||||
right: current_rect.right,
|
||||
bottom: current_rect.bottom,
|
||||
};
|
||||
//TODO: We might need to take into account the differences in DPI for the new_rect, unless
|
||||
//we can use the xy ratios above to the right/bottom (width/height of window) as well?
|
||||
|
||||
self.set_position(&new_rect, true)
|
||||
}
|
||||
|
||||
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
|
||||
let half_width = work_area.right / 2;
|
||||
let half_weight = work_area.bottom / 2;
|
||||
|
||||
@@ -151,25 +199,77 @@ impl Window {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn animate_position(&self, start_rect: &Rect, target_rect: &Rect, top: bool) -> Result<()> {
|
||||
let start_rect = *start_rect;
|
||||
let target_rect = *target_rect;
|
||||
let duration = Duration::from_millis(ANIMATION_DURATION.load(Ordering::SeqCst));
|
||||
let mut animation = self.animation;
|
||||
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
border_manager::send_notification(Some(self.hwnd));
|
||||
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
let hwnd = self.hwnd;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
animation.animate(duration, |progress: f64| {
|
||||
let new_rect = Animation::lerp_rect(&start_rect, &target_rect, progress);
|
||||
|
||||
if progress == 1.0 {
|
||||
WindowsApi::position_window(hwnd, &new_rect, top)?;
|
||||
if WindowsApi::foreground_window().unwrap_or_default() == hwnd {
|
||||
focus_manager::send_notification(hwnd)
|
||||
}
|
||||
|
||||
if ANIMATIONS_IN_PROGRESS.load(Ordering::Acquire) == 0 {
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED
|
||||
.store(false, Ordering::SeqCst);
|
||||
|
||||
border_manager::send_notification(Some(hwnd));
|
||||
stackbar_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
}
|
||||
} else {
|
||||
// using MoveWindow because it runs faster than SetWindowPos
|
||||
// so animation have more fps and feel smoother
|
||||
WindowsApi::move_window(hwnd, &new_rect, false)?;
|
||||
WindowsApi::invalidate_rect(hwnd, None, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
|
||||
if WindowsApi::window_rect(self.hwnd())?.eq(layout) {
|
||||
let window_rect = WindowsApi::window_rect(self.hwnd)?;
|
||||
|
||||
if window_rect.eq(layout) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rect = *layout;
|
||||
WindowsApi::position_window(self.hwnd(), &rect, top)
|
||||
if ANIMATION_ENABLED.load(Ordering::SeqCst) {
|
||||
self.animate_position(&window_rect, layout, top)
|
||||
} else {
|
||||
WindowsApi::position_window(self.hwnd, layout, top)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_maximized(self) -> bool {
|
||||
WindowsApi::is_zoomed(self.hwnd())
|
||||
WindowsApi::is_zoomed(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn is_miminized(self) -> bool {
|
||||
WindowsApi::is_iconic(self.hwnd())
|
||||
WindowsApi::is_iconic(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn is_visible(self) -> bool {
|
||||
WindowsApi::is_window_visible(self.hwnd())
|
||||
WindowsApi::is_window_visible(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn hide(self) {
|
||||
@@ -180,8 +280,8 @@ impl Window {
|
||||
|
||||
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
|
||||
match *hiding_behaviour {
|
||||
HidingBehaviour::Hide => WindowsApi::hide_window(self.hwnd()),
|
||||
HidingBehaviour::Minimize => WindowsApi::minimize_window(self.hwnd()),
|
||||
HidingBehaviour::Hide => WindowsApi::hide_window(self.hwnd),
|
||||
HidingBehaviour::Minimize => WindowsApi::minimize_window(self.hwnd),
|
||||
HidingBehaviour::Cloak => SetCloak(self.hwnd(), 1, 2),
|
||||
}
|
||||
}
|
||||
@@ -198,18 +298,21 @@ impl Window {
|
||||
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
|
||||
match *hiding_behaviour {
|
||||
HidingBehaviour::Hide | HidingBehaviour::Minimize => {
|
||||
WindowsApi::restore_window(self.hwnd());
|
||||
WindowsApi::restore_window(self.hwnd);
|
||||
}
|
||||
HidingBehaviour::Cloak => SetCloak(self.hwnd(), 1, 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn minimize(self) {
|
||||
WindowsApi::minimize_window(self.hwnd());
|
||||
let exe = self.exe().unwrap_or_default();
|
||||
if !exe.contains("komorebi-bar") {
|
||||
WindowsApi::minimize_window(self.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(self) -> Result<()> {
|
||||
WindowsApi::close_window(self.hwnd())
|
||||
WindowsApi::close_window(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn maximize(self) {
|
||||
@@ -221,7 +324,7 @@ impl Window {
|
||||
programmatically_hidden_hwnds.remove(idx);
|
||||
}
|
||||
|
||||
WindowsApi::maximize_window(self.hwnd());
|
||||
WindowsApi::maximize_window(self.hwnd);
|
||||
}
|
||||
|
||||
pub fn unmaximize(self) {
|
||||
@@ -233,27 +336,27 @@ impl Window {
|
||||
programmatically_hidden_hwnds.remove(idx);
|
||||
}
|
||||
|
||||
WindowsApi::unmaximize_window(self.hwnd());
|
||||
WindowsApi::unmaximize_window(self.hwnd);
|
||||
}
|
||||
|
||||
pub fn focus(self, mouse_follows_focus: bool) -> Result<()> {
|
||||
// If the target window is already focused, do nothing.
|
||||
if let Ok(ihwnd) = WindowsApi::foreground_window() {
|
||||
if HWND(ihwnd) == self.hwnd() {
|
||||
if ihwnd == self.hwnd {
|
||||
// Center cursor in Window
|
||||
if mouse_follows_focus {
|
||||
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd())?)?;
|
||||
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd)?)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
WindowsApi::raise_and_focus_window(self.hwnd())?;
|
||||
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())?)?;
|
||||
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(self.hwnd)?)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -264,7 +367,7 @@ impl Window {
|
||||
ex_style.insert(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)?;
|
||||
WindowsApi::set_transparent(
|
||||
self.hwnd(),
|
||||
self.hwnd,
|
||||
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
|
||||
)
|
||||
}
|
||||
@@ -275,31 +378,50 @@ impl Window {
|
||||
self.update_ex_style(&ex_style)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn update_style(self, style: &WindowStyle) -> Result<()> {
|
||||
WindowsApi::update_style(self.hwnd(), isize::try_from(style.bits())?)
|
||||
pub fn set_accent(self, colour: u32) -> Result<()> {
|
||||
WindowsApi::set_window_accent(self.hwnd, Some(colour))
|
||||
}
|
||||
|
||||
pub fn remove_accent(self) -> Result<()> {
|
||||
WindowsApi::set_window_accent(self.hwnd, None)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
pub fn update_style(self, style: &WindowStyle) -> Result<()> {
|
||||
WindowsApi::update_style(self.hwnd, isize::try_from(style.bits())?)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
pub fn update_style(self, style: &WindowStyle) -> Result<()> {
|
||||
WindowsApi::update_style(self.hwnd, i32::try_from(style.bits())?)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
pub fn update_ex_style(self, style: &ExtendedWindowStyle) -> Result<()> {
|
||||
WindowsApi::update_ex_style(self.hwnd(), isize::try_from(style.bits())?)
|
||||
WindowsApi::update_ex_style(self.hwnd, isize::try_from(style.bits())?)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
pub fn update_ex_style(self, style: &ExtendedWindowStyle) -> Result<()> {
|
||||
WindowsApi::update_ex_style(self.hwnd, i32::try_from(style.bits())?)
|
||||
}
|
||||
|
||||
pub fn style(self) -> Result<WindowStyle> {
|
||||
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd())?)?;
|
||||
let bits = u32::try_from(WindowsApi::gwl_style(self.hwnd)?)?;
|
||||
Ok(WindowStyle::from_bits_truncate(bits))
|
||||
}
|
||||
|
||||
pub fn ex_style(self) -> Result<ExtendedWindowStyle> {
|
||||
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd())?)?;
|
||||
let bits = u32::try_from(WindowsApi::gwl_ex_style(self.hwnd)?)?;
|
||||
Ok(ExtendedWindowStyle::from_bits_truncate(bits))
|
||||
}
|
||||
|
||||
pub fn title(self) -> Result<String> {
|
||||
WindowsApi::window_text_w(self.hwnd())
|
||||
WindowsApi::window_text_w(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn path(self) -> Result<String> {
|
||||
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
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)?;
|
||||
@@ -307,23 +429,28 @@ impl Window {
|
||||
}
|
||||
|
||||
pub fn exe(self) -> Result<String> {
|
||||
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd());
|
||||
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd);
|
||||
let handle = WindowsApi::process_handle(process_id)?;
|
||||
let exe = WindowsApi::exe(handle);
|
||||
WindowsApi::close_process(handle)?;
|
||||
exe
|
||||
}
|
||||
|
||||
pub fn process_id(self) -> u32 {
|
||||
let (process_id, _) = WindowsApi::window_thread_process_id(self.hwnd);
|
||||
process_id
|
||||
}
|
||||
|
||||
pub fn class(self) -> Result<String> {
|
||||
WindowsApi::real_window_class_w(self.hwnd())
|
||||
WindowsApi::real_window_class_w(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn is_cloaked(self) -> Result<bool> {
|
||||
WindowsApi::is_window_cloaked(self.hwnd())
|
||||
WindowsApi::is_window_cloaked(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn is_window(self) -> bool {
|
||||
WindowsApi::is_window(self.hwnd())
|
||||
WindowsApi::is_window(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn remove_title_bar(self) -> Result<()> {
|
||||
@@ -352,6 +479,20 @@ impl Window {
|
||||
|
||||
debug.is_window = true;
|
||||
|
||||
let rect = WindowsApi::window_rect(self.hwnd).unwrap_or_default();
|
||||
|
||||
if rect.right < MINIMUM_WIDTH.load(Ordering::SeqCst) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
debug.has_minimum_width = true;
|
||||
|
||||
if rect.bottom < MINIMUM_HEIGHT.load(Ordering::SeqCst) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
debug.has_minimum_height = true;
|
||||
|
||||
if self.title().is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -408,6 +549,8 @@ impl Window {
|
||||
pub struct RuleDebug {
|
||||
pub should_manage: bool,
|
||||
pub is_window: bool,
|
||||
pub has_minimum_width: bool,
|
||||
pub has_minimum_height: bool,
|
||||
pub has_title: bool,
|
||||
pub is_cloaked: bool,
|
||||
pub allow_cloaked: bool,
|
||||
@@ -419,7 +562,7 @@ pub struct RuleDebug {
|
||||
pub class: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub matches_permaignore_class: Option<String>,
|
||||
pub matches_float_identifier: Option<MatchingRule>,
|
||||
pub matches_ignore_identifier: Option<MatchingRule>,
|
||||
pub matches_managed_override: Option<MatchingRule>,
|
||||
pub matches_layered_whitelist: Option<MatchingRule>,
|
||||
pub matches_wsl2_gui: Option<String>,
|
||||
@@ -448,16 +591,16 @@ fn window_is_eligible(
|
||||
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
let float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
let ignore_identifiers = IGNORE_IDENTIFIERS.lock();
|
||||
let should_float = if let Some(rule) = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&float_identifiers,
|
||||
&ignore_identifiers,
|
||||
®ex_identifiers,
|
||||
) {
|
||||
debug.matches_float_identifier = Some(rule);
|
||||
debug.matches_ignore_identifier = Some(rule);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -521,8 +664,23 @@ fn window_is_eligible(
|
||||
titlebars_removed.contains(exe_name)
|
||||
};
|
||||
|
||||
if exe_name.contains("firefox") {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
{
|
||||
let slow_application_identifiers = SLOW_APPLICATION_IDENTIFIERS.lock();
|
||||
let should_sleep = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&slow_application_identifiers,
|
||||
®ex_identifiers,
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if should_sleep {
|
||||
std::thread::sleep(Duration::from_millis(
|
||||
SLOW_APPLICATION_COMPENSATION_TIME.load(Ordering::SeqCst),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (allow_wsl2_gui || allow_titlebar_removed || style.contains(WindowStyle::CAPTION) && ex_style.contains(ExtendedWindowStyle::WINDOWEDGE))
|
||||
|
||||
@@ -16,37 +16,41 @@ use hotwatch::notify::ErrorKind as NotifyErrorKind;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixListener;
|
||||
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::custom_layout::CustomLayout;
|
||||
use komorebi_core::Arrangement;
|
||||
use komorebi_core::Axis;
|
||||
use komorebi_core::BorderStyle;
|
||||
use komorebi_core::CycleDirection;
|
||||
use komorebi_core::DefaultLayout;
|
||||
use komorebi_core::FocusFollowsMouseImplementation;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Layout;
|
||||
use komorebi_core::MoveBehaviour;
|
||||
use komorebi_core::OperationBehaviour;
|
||||
use komorebi_core::OperationDirection;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::Sizing;
|
||||
use komorebi_core::StackbarLabel;
|
||||
use komorebi_core::WindowContainerBehaviour;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::custom_layout::CustomLayout;
|
||||
use crate::core::Arrangement;
|
||||
use crate::core::Axis;
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::CycleDirection;
|
||||
use crate::core::DefaultLayout;
|
||||
use crate::core::FocusFollowsMouseImplementation;
|
||||
use crate::core::HidingBehaviour;
|
||||
use crate::core::Layout;
|
||||
use crate::core::MoveBehaviour;
|
||||
use crate::core::OperationBehaviour;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
use crate::core::Sizing;
|
||||
use crate::core::StackbarLabel;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowManagementBehaviour;
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::container::Container;
|
||||
use crate::core::StackbarMode;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::load_configuration;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
use crate::should_act_individual;
|
||||
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_LABEL;
|
||||
use crate::stackbar_manager::STACKBAR_MODE;
|
||||
@@ -63,23 +67,23 @@ use crate::winevent_listener;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::BorderColours;
|
||||
use crate::Colour;
|
||||
use crate::CrossBoundaryBehaviour;
|
||||
use crate::Rgb;
|
||||
use crate::WorkspaceRule;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::HOME_DIR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_RULES;
|
||||
use komorebi_core::StackbarMode;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowManager {
|
||||
@@ -89,8 +93,9 @@ pub struct WindowManager {
|
||||
pub is_paused: bool,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub resize_delta: i32,
|
||||
pub window_container_behaviour: WindowContainerBehaviour,
|
||||
pub window_management_behaviour: WindowManagementBehaviour,
|
||||
pub cross_monitor_move_behaviour: MoveBehaviour,
|
||||
pub cross_boundary_behaviour: CrossBoundaryBehaviour,
|
||||
pub unmanaged_window_operation_behaviour: OperationBehaviour,
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
pub mouse_follows_focus: bool,
|
||||
@@ -108,6 +113,7 @@ pub struct State {
|
||||
pub is_paused: bool,
|
||||
pub resize_delta: i32,
|
||||
pub new_window_behaviour: WindowContainerBehaviour,
|
||||
pub float_override: bool,
|
||||
pub cross_monitor_move_behaviour: MoveBehaviour,
|
||||
pub unmanaged_window_operation_behaviour: OperationBehaviour,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
@@ -116,6 +122,54 @@ pub struct State {
|
||||
pub has_pending_raise_op: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn has_been_modified(&self, wm: &WindowManager) -> bool {
|
||||
let new = Self::from(wm);
|
||||
|
||||
if self.monitors != new.monitors {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.is_paused != new.is_paused {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.new_window_behaviour != new.new_window_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.float_override != new.float_override {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.cross_monitor_move_behaviour != new.cross_monitor_move_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.unmanaged_window_operation_behaviour != new.unmanaged_window_operation_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.work_area_offset != new.work_area_offset {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.focus_follows_mouse != new.focus_follows_mouse {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.mouse_follows_focus != new.mouse_follows_focus {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.has_pending_raise_op != new.has_pending_raise_op {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GlobalState {
|
||||
@@ -132,14 +186,15 @@ pub struct GlobalState {
|
||||
pub stackbar_tab_width: i32,
|
||||
pub stackbar_height: i32,
|
||||
pub remove_titlebars: bool,
|
||||
pub float_identifiers: Vec<MatchingRule>,
|
||||
#[serde(alias = "float_identifiers")]
|
||||
pub ignore_identifiers: Vec<MatchingRule>,
|
||||
pub manage_identifiers: Vec<MatchingRule>,
|
||||
pub layered_whitelist: Vec<MatchingRule>,
|
||||
pub tray_and_multi_window_identifiers: Vec<MatchingRule>,
|
||||
pub name_change_on_launch_identifiers: Vec<MatchingRule>,
|
||||
pub monitor_index_preferences: HashMap<usize, Rect>,
|
||||
pub display_index_preferences: HashMap<usize, String>,
|
||||
pub workspace_rules: HashMap<String, WorkspaceRule>,
|
||||
pub workspace_rules: Vec<WorkspaceMatchingRule>,
|
||||
pub window_hiding_behaviour: HidingBehaviour,
|
||||
pub configuration_dir: PathBuf,
|
||||
pub data_dir: PathBuf,
|
||||
@@ -160,6 +215,9 @@ impl Default for GlobalState {
|
||||
monocle: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::MONOCLE.load(Ordering::SeqCst),
|
||||
))),
|
||||
floating: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::FLOATING.load(Ordering::SeqCst),
|
||||
))),
|
||||
unfocused: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::UNFOCUSED.load(Ordering::SeqCst),
|
||||
))),
|
||||
@@ -181,14 +239,14 @@ impl Default for GlobalState {
|
||||
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
|
||||
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
|
||||
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
|
||||
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
|
||||
ignore_identifiers: IGNORE_IDENTIFIERS.lock().clone(),
|
||||
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
|
||||
layered_whitelist: LAYERED_WHITELIST.lock().clone(),
|
||||
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
|
||||
name_change_on_launch_identifiers: OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
|
||||
monitor_index_preferences: MONITOR_INDEX_PREFERENCES.lock().clone(),
|
||||
display_index_preferences: DISPLAY_INDEX_PREFERENCES.lock().clone(),
|
||||
workspace_rules: WORKSPACE_RULES.lock().clone(),
|
||||
workspace_rules: WORKSPACE_MATCHING_RULES.lock().clone(),
|
||||
window_hiding_behaviour: *HIDING_BEHAVIOUR.lock(),
|
||||
configuration_dir: HOME_DIR.clone(),
|
||||
data_dir: DATA_DIR.clone(),
|
||||
@@ -210,7 +268,8 @@ impl From<&WindowManager> for State {
|
||||
is_paused: wm.is_paused,
|
||||
work_area_offset: wm.work_area_offset,
|
||||
resize_delta: wm.resize_delta,
|
||||
new_window_behaviour: wm.window_container_behaviour,
|
||||
new_window_behaviour: wm.window_management_behaviour.current_behaviour,
|
||||
float_override: wm.window_management_behaviour.float_override,
|
||||
cross_monitor_move_behaviour: wm.cross_monitor_move_behaviour,
|
||||
focus_follows_mouse: wm.focus_follows_mouse,
|
||||
mouse_follows_focus: wm.mouse_follows_focus,
|
||||
@@ -230,7 +289,6 @@ struct EnforceWorkspaceRuleOp {
|
||||
target_monitor_idx: usize,
|
||||
target_workspace_idx: usize,
|
||||
}
|
||||
|
||||
impl EnforceWorkspaceRuleOp {
|
||||
const fn is_origin(&self, monitor_idx: usize, workspace_idx: usize) -> bool {
|
||||
self.origin_monitor_idx == monitor_idx && self.origin_workspace_idx == workspace_idx
|
||||
@@ -271,8 +329,9 @@ impl WindowManager {
|
||||
is_paused: false,
|
||||
virtual_desktop_id: current_virtual_desktop(),
|
||||
work_area_offset: None,
|
||||
window_container_behaviour: WindowContainerBehaviour::Create,
|
||||
window_management_behaviour: WindowManagementBehaviour::default(),
|
||||
cross_monitor_move_behaviour: MoveBehaviour::Swap,
|
||||
cross_boundary_behaviour: CrossBoundaryBehaviour::Workspace,
|
||||
unmanaged_window_operation_behaviour: OperationBehaviour::Op,
|
||||
resize_delta: 50,
|
||||
focus_follows_mouse: None,
|
||||
@@ -303,6 +362,54 @@ impl WindowManager {
|
||||
StaticConfig::reload(pathbuf, self)
|
||||
}
|
||||
|
||||
pub fn window_management_behaviour(
|
||||
&self,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
) -> WindowManagementBehaviour {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.workspaces().get(workspace_idx) {
|
||||
let current_behaviour =
|
||||
if let Some(behaviour) = workspace.window_container_behaviour() {
|
||||
if workspace.containers().is_empty()
|
||||
&& matches!(behaviour, WindowContainerBehaviour::Append)
|
||||
{
|
||||
// You can't append to an empty workspace
|
||||
WindowContainerBehaviour::Create
|
||||
} else {
|
||||
*behaviour
|
||||
}
|
||||
} else if workspace.containers().is_empty()
|
||||
&& matches!(
|
||||
self.window_management_behaviour.current_behaviour,
|
||||
WindowContainerBehaviour::Append
|
||||
)
|
||||
{
|
||||
// You can't append to an empty workspace
|
||||
WindowContainerBehaviour::Create
|
||||
} else {
|
||||
self.window_management_behaviour.current_behaviour
|
||||
};
|
||||
|
||||
let float_override = if let Some(float_override) = workspace.float_override() {
|
||||
*float_override
|
||||
} else {
|
||||
self.window_management_behaviour.float_override
|
||||
};
|
||||
|
||||
return WindowManagementBehaviour {
|
||||
current_behaviour,
|
||||
float_override,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
WindowManagementBehaviour {
|
||||
current_behaviour: WindowContainerBehaviour::Create,
|
||||
float_override: self.window_management_behaviour.float_override,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn watch_configuration(&mut self, enable: bool) -> Result<()> {
|
||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
@@ -428,7 +535,8 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
|
||||
.focused_workspace_idx();
|
||||
|
||||
let workspace_rules = WORKSPACE_RULES.lock();
|
||||
let workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
// Go through all the monitors and workspaces
|
||||
for (i, monitor) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
@@ -438,63 +546,61 @@ impl WindowManager {
|
||||
let exe_name = window.exe()?;
|
||||
let title = window.title()?;
|
||||
let class = window.class()?;
|
||||
let path = window.path()?;
|
||||
|
||||
let mut found_workspace_rule = workspace_rules.get(&exe_name);
|
||||
|
||||
if found_workspace_rule.is_none() {
|
||||
found_workspace_rule = workspace_rules.get(&title);
|
||||
}
|
||||
|
||||
if found_workspace_rule.is_none() {
|
||||
found_workspace_rule = workspace_rules.get(&class);
|
||||
}
|
||||
|
||||
if found_workspace_rule.is_none() {
|
||||
for (k, v) in workspace_rules.iter() {
|
||||
if let Ok(re) = Regex::new(k) {
|
||||
if re.is_match(&exe_name) {
|
||||
found_workspace_rule = Some(v);
|
||||
for rule in &*workspace_matching_rules {
|
||||
let matched = match &rule.matching_rule {
|
||||
MatchingRule::Simple(r) => should_act_individual(
|
||||
&title,
|
||||
&exe_name,
|
||||
&class,
|
||||
&path,
|
||||
r,
|
||||
®ex_identifiers,
|
||||
),
|
||||
MatchingRule::Composite(r) => {
|
||||
let mut composite_results = vec![];
|
||||
for identifier in r {
|
||||
composite_results.push(should_act_individual(
|
||||
&title,
|
||||
&exe_name,
|
||||
&class,
|
||||
&path,
|
||||
identifier,
|
||||
®ex_identifiers,
|
||||
));
|
||||
}
|
||||
|
||||
if re.is_match(&title) {
|
||||
found_workspace_rule = Some(v);
|
||||
}
|
||||
|
||||
if re.is_match(&class) {
|
||||
found_workspace_rule = Some(v);
|
||||
}
|
||||
composite_results.iter().all(|&x| x)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If the executable names or titles of any of those windows are in our rules map
|
||||
if let Some((monitor_idx, workspace_idx, apply_on_first_show_only)) =
|
||||
found_workspace_rule
|
||||
{
|
||||
if *apply_on_first_show_only {
|
||||
if !already_moved_window_handles.contains(&window.hwnd) {
|
||||
already_moved_window_handles.insert(window.hwnd);
|
||||
if matched {
|
||||
if rule.initial_only {
|
||||
if !already_moved_window_handles.contains(&window.hwnd) {
|
||||
already_moved_window_handles.insert(window.hwnd);
|
||||
|
||||
self.add_window_handle_to_move_based_on_workspace_rule(
|
||||
&window.title()?,
|
||||
window.hwnd,
|
||||
i,
|
||||
j,
|
||||
rule.monitor_index,
|
||||
rule.workspace_index,
|
||||
&mut to_move,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.add_window_handle_to_move_based_on_workspace_rule(
|
||||
&window.title()?,
|
||||
window.hwnd,
|
||||
i,
|
||||
j,
|
||||
*monitor_idx,
|
||||
*workspace_idx,
|
||||
rule.monitor_index,
|
||||
rule.workspace_index,
|
||||
&mut to_move,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.add_window_handle_to_move_based_on_workspace_rule(
|
||||
&window.title()?,
|
||||
window.hwnd,
|
||||
i,
|
||||
j,
|
||||
*monitor_idx,
|
||||
*workspace_idx,
|
||||
&mut to_move,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -797,35 +903,46 @@ impl WindowManager {
|
||||
let rect = self.focused_monitor_size()?;
|
||||
WindowsApi::center_cursor_in_rect(&rect)?;
|
||||
|
||||
match WindowsApi::raise_and_focus_window(desktop_window.hwnd()) {
|
||||
match WindowsApi::raise_and_focus_window(desktop_window.hwnd) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::warn!("{} {}:{}", error, file!(), line!());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.focused_workspace()?.is_empty() {
|
||||
let desktop_window = Window::from(WindowsApi::desktop_window()?);
|
||||
|
||||
// if we passed false for follow_focus and there is a container on the workspace
|
||||
if !follow_focus && self.focused_container_mut().is_ok() {
|
||||
// and we have a stack with >1 windows
|
||||
if self.focused_container_mut()?.windows().len() > 1
|
||||
// and we don't have a maxed window
|
||||
&& self.focused_workspace()?.maximized_window().is_none()
|
||||
// and we don't have a monocle container
|
||||
&& self.focused_workspace()?.monocle_container().is_none()
|
||||
{
|
||||
if let Ok(window) = self.focused_window_mut() {
|
||||
if trigger_focus {
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
match WindowsApi::raise_and_focus_window(desktop_window.hwnd) {
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::warn!("{} {}:{}", error, file!(), line!());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is to correctly restore and focus when switching to a workspace which
|
||||
// contains a managed maximized window
|
||||
if !follow_focus {
|
||||
// if we passed false for follow_focus and there is a container on the workspace
|
||||
if self.focused_container_mut().is_ok() {
|
||||
// and we have a stack with >1 windows
|
||||
if self.focused_container_mut()?.windows().len() > 1
|
||||
// and we don't have a maxed window
|
||||
&& self.focused_workspace()?.maximized_window().is_none()
|
||||
// and we don't have a monocle container
|
||||
&& self.focused_workspace()?.monocle_container().is_none()
|
||||
// and we don't have any floating windows that should show on top
|
||||
&& self.focused_workspace()?.floating_windows().is_empty()
|
||||
{
|
||||
if let Ok(window) = self.focused_window_mut() {
|
||||
if trigger_focus {
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is to correctly restore and focus when switching to a workspace which
|
||||
// contains a managed maximized window
|
||||
if let Some(window) = self.focused_workspace()?.maximized_window() {
|
||||
window.restore();
|
||||
if trigger_focus {
|
||||
@@ -934,9 +1051,18 @@ impl WindowManager {
|
||||
|
||||
let no_titlebar = NO_TITLEBAR.lock();
|
||||
let known_transparent_hwnds = transparency_manager::known_hwnds();
|
||||
let border_implementation = border_manager::IMPLEMENTATION.load();
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
for workspace in monitor.workspaces_mut() {
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
for window in monocle.windows() {
|
||||
if matches!(border_implementation, BorderImplementation::Windows) {
|
||||
window.remove_accent()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for containers in workspace.containers_mut() {
|
||||
for window in containers.windows_mut() {
|
||||
if no_titlebar.contains(&window.exe()?) {
|
||||
@@ -947,6 +1073,10 @@ impl WindowManager {
|
||||
window.opaque()?;
|
||||
}
|
||||
|
||||
if matches!(border_implementation, BorderImplementation::Windows) {
|
||||
window.remove_accent()?;
|
||||
}
|
||||
|
||||
window.restore();
|
||||
}
|
||||
}
|
||||
@@ -956,6 +1086,29 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn remove_all_accents(&mut self) -> Result<()> {
|
||||
tracing::info!("removing all window accents");
|
||||
|
||||
for monitor in self.monitors() {
|
||||
for workspace in monitor.workspaces() {
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
for window in monocle.windows() {
|
||||
window.remove_accent()?
|
||||
}
|
||||
}
|
||||
|
||||
for containers in workspace.containers() {
|
||||
for window in containers.windows() {
|
||||
window.remove_accent()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn handle_unmanaged_window_behaviour(&self) -> Result<()> {
|
||||
if matches!(
|
||||
@@ -1072,7 +1225,7 @@ impl WindowManager {
|
||||
|
||||
if focused_monitor_idx == monitor_idx {
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
return self.move_container_to_workspace(workspace_idx, follow);
|
||||
return self.move_container_to_workspace(workspace_idx, follow, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1083,6 +1236,8 @@ impl WindowManager {
|
||||
.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
let current_area = *monitor.work_area_size();
|
||||
|
||||
let workspace = monitor
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?;
|
||||
@@ -1091,16 +1246,23 @@ impl WindowManager {
|
||||
bail!("cannot move native maximized window to another monitor or workspace");
|
||||
}
|
||||
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
let container_hwnds = container
|
||||
.windows()
|
||||
let foreground_hwnd = WindowsApi::foreground_window()?;
|
||||
let floating_window_index = workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
.position(|w| w.hwnd == foreground_hwnd);
|
||||
|
||||
let floating_window =
|
||||
floating_window_index.map(|idx| workspace.floating_windows_mut().remove(idx));
|
||||
let container = if floating_window_index.is_none() {
|
||||
Some(
|
||||
workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
|
||||
let target_monitor = self
|
||||
@@ -1108,18 +1270,36 @@ impl WindowManager {
|
||||
.get_mut(monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
target_monitor.add_container(container, workspace_idx)?;
|
||||
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
target_monitor.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
let target_workspace = target_monitor
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused workspace on target monitor"))?;
|
||||
|
||||
if let Some(workspace) = target_monitor.focused_workspace() {
|
||||
if !*workspace.tile() {
|
||||
for hwnd in container_hwnds {
|
||||
Window::from(hwnd).center(target_monitor.work_area_size())?;
|
||||
if let Some(window) = floating_window {
|
||||
target_workspace.floating_windows_mut().push(window);
|
||||
Window::from(window.hwnd)
|
||||
.move_to_area(¤t_area, target_monitor.work_area_size())?;
|
||||
} else if let Some(container) = container {
|
||||
let container_hwnds = container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
target_monitor.add_container(container, workspace_idx)?;
|
||||
|
||||
if let Some(workspace) = target_monitor.focused_workspace() {
|
||||
if !*workspace.tile() {
|
||||
for hwnd in container_hwnds {
|
||||
Window::from(hwnd)
|
||||
.move_to_area(¤t_area, target_monitor.work_area_size())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("failed to find a window to move");
|
||||
}
|
||||
|
||||
target_monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
@@ -1134,11 +1314,18 @@ impl WindowManager {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn move_container_to_workspace(&mut self, idx: usize, follow: bool) -> Result<()> {
|
||||
pub fn move_container_to_workspace(
|
||||
&mut self,
|
||||
idx: usize,
|
||||
follow: bool,
|
||||
direction: Option<OperationDirection>,
|
||||
) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
tracing::info!("moving container");
|
||||
@@ -1148,10 +1335,12 @@ impl WindowManager {
|
||||
.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
monitor.move_container_to_workspace(idx, follow)?;
|
||||
monitor.move_container_to_workspace(idx, follow, direction)?;
|
||||
monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
|
||||
self.update_focused_workspace(mouse_follows_focus, true)
|
||||
self.update_focused_workspace(mouse_follows_focus, true)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_focused_workspace(&mut self) -> Option<Workspace> {
|
||||
@@ -1188,6 +1377,7 @@ impl WindowManager {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
let workspace = self.focused_workspace()?;
|
||||
let workspace_idx = self.focused_workspace_idx()?;
|
||||
|
||||
tracing::info!("focusing container");
|
||||
|
||||
@@ -1199,6 +1389,70 @@ impl WindowManager {
|
||||
|
||||
let mut cross_monitor_monocle = false;
|
||||
|
||||
// this is for when we are scrolling across workspaces like PaperWM
|
||||
if new_idx.is_none()
|
||||
&& matches!(
|
||||
self.cross_boundary_behaviour,
|
||||
CrossBoundaryBehaviour::Workspace
|
||||
)
|
||||
&& matches!(
|
||||
direction,
|
||||
OperationDirection::Left | OperationDirection::Right
|
||||
)
|
||||
{
|
||||
let workspace_count = if let Some(monitor) = self.focused_monitor() {
|
||||
monitor.workspaces().len()
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
let next_idx = match direction {
|
||||
OperationDirection::Left => match workspace_idx {
|
||||
0 => workspace_count - 1,
|
||||
n => n - 1,
|
||||
},
|
||||
OperationDirection::Right => match workspace_idx {
|
||||
n if n == workspace_count - 1 => 0,
|
||||
n => n + 1,
|
||||
},
|
||||
_ => workspace_idx,
|
||||
};
|
||||
|
||||
self.focus_workspace(next_idx)?;
|
||||
|
||||
if let Ok(focused_workspace) = self.focused_workspace_mut() {
|
||||
if focused_workspace.monocle_container().is_none() {
|
||||
match direction {
|
||||
OperationDirection::Left => match focused_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.rightmost_index(focused_workspace.containers().len());
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
focused_workspace.focus_container(
|
||||
focused_workspace.containers().len().saturating_sub(1),
|
||||
);
|
||||
}
|
||||
},
|
||||
OperationDirection::Right => match focused_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(focused_workspace.containers().len());
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
focused_workspace.focus_container(0);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// if there is no container in that direction for this workspace
|
||||
match new_idx {
|
||||
None => {
|
||||
@@ -1207,17 +1461,44 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
|
||||
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
|
||||
if let Ok(focused_workspace) = self.focused_workspace() {
|
||||
if let Ok(focused_workspace) = self.focused_workspace_mut() {
|
||||
if let Some(monocle) = focused_workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
window.focus(mouse_follows_focus)?;
|
||||
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
|
||||
window.hwnd(),
|
||||
window.hwnd,
|
||||
)?)?;
|
||||
|
||||
cross_monitor_monocle = true;
|
||||
}
|
||||
} else {
|
||||
match direction {
|
||||
OperationDirection::Left => match focused_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index = layout
|
||||
.rightmost_index(focused_workspace.containers().len());
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
focused_workspace.focus_container(
|
||||
focused_workspace.containers().len().saturating_sub(1),
|
||||
);
|
||||
}
|
||||
},
|
||||
OperationDirection::Right => match focused_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(focused_workspace.containers().len());
|
||||
focused_workspace.focus_container(target_index);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
focused_workspace.focus_container(0);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1241,6 +1522,7 @@ impl WindowManager {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
let workspace = self.focused_workspace()?;
|
||||
let workspace_idx = self.focused_workspace_idx()?;
|
||||
|
||||
// removing this messes up the monitor / container / window index somewhere
|
||||
// and results in the wrong window getting moved across the monitor boundary
|
||||
@@ -1254,6 +1536,43 @@ impl WindowManager {
|
||||
let origin_monitor_idx = self.focused_monitor_idx();
|
||||
let target_container_idx = workspace.new_idx_for_direction(direction);
|
||||
|
||||
// this is for when we are scrolling across workspaces like PaperWM
|
||||
if target_container_idx.is_none()
|
||||
&& matches!(
|
||||
self.cross_boundary_behaviour,
|
||||
CrossBoundaryBehaviour::Workspace
|
||||
)
|
||||
&& matches!(
|
||||
direction,
|
||||
OperationDirection::Left | OperationDirection::Right
|
||||
)
|
||||
{
|
||||
let workspace_count = if let Some(monitor) = self.focused_monitor() {
|
||||
monitor.workspaces().len()
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
let next_idx = match direction {
|
||||
OperationDirection::Left => match workspace_idx {
|
||||
0 => workspace_count - 1,
|
||||
n => n - 1,
|
||||
},
|
||||
OperationDirection::Right => match workspace_idx {
|
||||
n if n == workspace_count - 1 => 0,
|
||||
n => n + 1,
|
||||
},
|
||||
_ => workspace_idx,
|
||||
};
|
||||
|
||||
// passing the direction here is how we handle whether to insert at the front
|
||||
// or the back of the container vecdeque in the target workspace
|
||||
self.move_container_to_workspace(next_idx, true, Some(direction))?;
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match target_container_idx {
|
||||
// If there is nowhere to move on the current workspace, try to move it onto the monitor
|
||||
// in that direction if there is one
|
||||
@@ -1296,12 +1615,78 @@ impl WindowManager {
|
||||
// get a mutable ref to the focused workspace on the target monitor
|
||||
let target_workspace = self.focused_workspace_mut()?;
|
||||
|
||||
// insert the origin container into the focused workspace on the target monitor
|
||||
// at the position where the currently focused container on that workspace is
|
||||
target_workspace.insert_container_at_idx(
|
||||
target_workspace.focused_container_idx(),
|
||||
origin_container,
|
||||
);
|
||||
match direction {
|
||||
OperationDirection::Left => {
|
||||
// insert the origin container into the focused workspace on the target monitor
|
||||
// at the back (or rightmost position) if we are moving across a boundary to
|
||||
// the left (back = right side of the target)
|
||||
match target_workspace.layout() {
|
||||
Layout::Default(layout) => match layout {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
target_workspace.add_container_to_front(origin_container);
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace
|
||||
.insert_container_at_idx(0, origin_container);
|
||||
} else {
|
||||
target_workspace
|
||||
.add_container_to_back(origin_container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.add_container_to_back(origin_container);
|
||||
}
|
||||
},
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_back(origin_container);
|
||||
}
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
// insert the origin container into the focused workspace on the target monitor
|
||||
// at the front (or leftmost position) if we are moving across a boundary to the
|
||||
// right (front = left side of the target)
|
||||
match target_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(target_workspace.containers().len());
|
||||
|
||||
match layout {
|
||||
DefaultLayout::RightMainVerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace
|
||||
.add_container_to_back(origin_container);
|
||||
} else {
|
||||
target_workspace.insert_container_at_idx(
|
||||
target_index,
|
||||
origin_container,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.insert_container_at_idx(
|
||||
target_index,
|
||||
origin_container,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_front(origin_container);
|
||||
}
|
||||
}
|
||||
}
|
||||
OperationDirection::Up | OperationDirection::Down => {
|
||||
// insert the origin container into the focused workspace on the target monitor
|
||||
// at the position where the currently focused container on that workspace is
|
||||
target_workspace.insert_container_at_idx(
|
||||
target_workspace.focused_container_idx(),
|
||||
origin_container,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// if there is only one container on the target workspace after the insertion
|
||||
// it means that there won't be one swapped back, so we have to decrement the
|
||||
@@ -1378,7 +1763,9 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -1463,6 +1850,31 @@ impl WindowManager {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn focus_container_window(&mut self, idx: usize) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
tracing::info!("focusing container window at index {idx}");
|
||||
|
||||
let container = self.focused_container_mut()?;
|
||||
|
||||
let len = NonZeroUsize::new(container.windows().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
|
||||
|
||||
if len.get() == 1 {
|
||||
bail!("there is only one window in this container");
|
||||
}
|
||||
|
||||
if container.windows().get(idx).is_none() {
|
||||
bail!("there is no window in this container at index {idx}");
|
||||
}
|
||||
|
||||
container.focus_window(idx);
|
||||
container.load_focused_window();
|
||||
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn stack_all(&mut self) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
@@ -2323,7 +2735,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
pub fn monitor_idx_from_window(&mut self, window: Window) -> Option<usize> {
|
||||
let hmonitor = WindowsApi::monitor_from_window(window.hwnd());
|
||||
let hmonitor = WindowsApi::monitor_from_window(window.hwnd);
|
||||
|
||||
for (i, monitor) in self.monitors().iter().enumerate() {
|
||||
if monitor.id() == hmonitor {
|
||||
|
||||
@@ -102,6 +102,48 @@ impl WindowManagerEvent {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn hwnd(self) -> isize {
|
||||
self.window().hwnd
|
||||
}
|
||||
|
||||
pub const fn title(self) -> &'static str {
|
||||
match self {
|
||||
WindowManagerEvent::Destroy(_, _) => "Destroy",
|
||||
WindowManagerEvent::FocusChange(_, _) => "FocusChange",
|
||||
WindowManagerEvent::Hide(_, _) => "Hide",
|
||||
WindowManagerEvent::Cloak(_, _) => "Cloak",
|
||||
WindowManagerEvent::Minimize(_, _) => "Minimize",
|
||||
WindowManagerEvent::Show(_, _) => "Show",
|
||||
WindowManagerEvent::Uncloak(_, _) => "Uncloak",
|
||||
WindowManagerEvent::MoveResizeStart(_, _) => "MoveResizeStart",
|
||||
WindowManagerEvent::MoveResizeEnd(_, _) => "MoveResizeEnd",
|
||||
WindowManagerEvent::MouseCapture(_, _) => "MouseCapture",
|
||||
WindowManagerEvent::Manage(_) => "Manage",
|
||||
WindowManagerEvent::Unmanage(_) => "Unmanage",
|
||||
WindowManagerEvent::Raise(_) => "Raise",
|
||||
WindowManagerEvent::TitleUpdate(_, _) => "TitleUpdate",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn winevent(self) -> Option<String> {
|
||||
match self {
|
||||
WindowManagerEvent::Destroy(event, _)
|
||||
| WindowManagerEvent::FocusChange(event, _)
|
||||
| WindowManagerEvent::Hide(event, _)
|
||||
| WindowManagerEvent::Cloak(event, _)
|
||||
| WindowManagerEvent::Minimize(event, _)
|
||||
| WindowManagerEvent::Show(event, _)
|
||||
| WindowManagerEvent::Uncloak(event, _)
|
||||
| WindowManagerEvent::MoveResizeStart(event, _)
|
||||
| WindowManagerEvent::MoveResizeEnd(event, _)
|
||||
| WindowManagerEvent::MouseCapture(event, _)
|
||||
| WindowManagerEvent::TitleUpdate(event, _) => Some(event.to_string()),
|
||||
WindowManagerEvent::Manage(_)
|
||||
| WindowManagerEvent::Unmanage(_)
|
||||
| WindowManagerEvent::Raise(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_win_event(winevent: WinEvent, window: Window) -> Option<Self> {
|
||||
match winevent {
|
||||
WinEvent::ObjectDestroy => Option::from(Self::Destroy(winevent, window)),
|
||||
@@ -151,7 +193,10 @@ impl WindowManagerEvent {
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if should_trigger_show {
|
||||
// should not trigger show on minimized windows, for example when firefox sends
|
||||
// this message due to youtube autoplay changing the window title
|
||||
// https://github.com/LGUG2Z/komorebi/issues/941
|
||||
if should_trigger_show && !window.is_miminized() {
|
||||
Option::from(Self::Show(winevent, window))
|
||||
} else {
|
||||
Option::from(Self::TitleUpdate(winevent, window))
|
||||
|
||||
@@ -14,14 +14,18 @@ use windows::Win32::Foundation::CloseHandle;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::HANDLE;
|
||||
use windows::Win32::Foundation::HINSTANCE;
|
||||
use windows::Win32::Foundation::HMODULE;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::POINT;
|
||||
use windows::Win32::Foundation::RECT;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
|
||||
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_BORDER_COLOR;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
|
||||
use windows::Win32::Graphics::Dwm::DWMWA_COLOR_NONE;
|
||||
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;
|
||||
@@ -32,6 +36,7 @@ use windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
|
||||
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
|
||||
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
|
||||
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::MonitorFromPoint;
|
||||
use windows::Win32::Graphics::Gdi::MonitorFromWindow;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
@@ -82,6 +87,7 @@ 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::MoveWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::RealGetWindowClassW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::RegisterClassW;
|
||||
@@ -129,7 +135,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
|
||||
|
||||
use komorebi_core::Rect;
|
||||
use crate::core::Rect;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::monitor;
|
||||
@@ -141,6 +147,14 @@ use crate::Window;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
|
||||
macro_rules! as_ptr {
|
||||
($value:expr) => {
|
||||
$value as *mut core::ffi::c_void
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use as_ptr;
|
||||
|
||||
pub enum WindowsResult<T, E> {
|
||||
Err(E),
|
||||
Ok(T),
|
||||
@@ -182,10 +196,10 @@ macro_rules! impl_process_windows_crate_integer_wrapper_result {
|
||||
$(
|
||||
impl ProcessWindowsCrateResult<$deref> for $input {
|
||||
fn process(self) -> Result<$deref> {
|
||||
if self == $input(0) {
|
||||
if self == $input(std::ptr::null_mut()) {
|
||||
Err(std::io::Error::last_os_error().into())
|
||||
} else {
|
||||
Ok(self.0)
|
||||
Ok(self.0 as $deref)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,9 +228,16 @@ impl WindowsApi {
|
||||
callback: MONITORENUMPROC,
|
||||
callback_data_address: isize,
|
||||
) -> Result<()> {
|
||||
unsafe { EnumDisplayMonitors(HDC(0), None, callback, LPARAM(callback_data_address)) }
|
||||
.ok()
|
||||
.process()
|
||||
unsafe {
|
||||
EnumDisplayMonitors(
|
||||
HDC(std::ptr::null_mut()),
|
||||
None,
|
||||
callback,
|
||||
LPARAM(callback_data_address),
|
||||
)
|
||||
}
|
||||
.ok()
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn valid_hmonitors() -> Result<Vec<(String, isize)>> {
|
||||
@@ -234,11 +255,17 @@ impl WindowsApi {
|
||||
pub fn load_monitor_information(monitors: &mut Ring<Monitor>) -> Result<()> {
|
||||
'read: for display in win32_display_data::connected_displays_all().flatten() {
|
||||
let path = display.device_path.clone();
|
||||
let mut split: Vec<_> = path.split('#').collect();
|
||||
split.remove(0);
|
||||
split.remove(split.len() - 1);
|
||||
let device = split[0].to_string();
|
||||
let device_id = split.join("-");
|
||||
|
||||
let (device, device_id) = if path.is_empty() {
|
||||
(String::from("UNKNOWN"), String::from("UNKNOWN"))
|
||||
} else {
|
||||
let mut split: Vec<_> = path.split('#').collect();
|
||||
split.remove(0);
|
||||
split.remove(split.len() - 1);
|
||||
let device = split[0].to_string();
|
||||
let device_id = split.join("-");
|
||||
(device, device_id)
|
||||
};
|
||||
|
||||
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
|
||||
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
|
||||
@@ -277,7 +304,7 @@ impl WindowsApi {
|
||||
monitors.elements_mut().push_back(m);
|
||||
} else if let Some(preference) = index_preference {
|
||||
while *preference > monitors.elements().len() {
|
||||
monitors.elements_mut().reserve(1);
|
||||
monitors.elements_mut().push_back(Monitor::placeholder());
|
||||
}
|
||||
|
||||
monitors.elements_mut().insert(*preference, m);
|
||||
@@ -286,6 +313,10 @@ impl WindowsApi {
|
||||
}
|
||||
}
|
||||
|
||||
monitors
|
||||
.elements_mut()
|
||||
.retain(|m| m.name().ne("PLACEHOLDER"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -313,8 +344,8 @@ impl WindowsApi {
|
||||
|
||||
for container in workspace.containers_mut() {
|
||||
for window in container.windows() {
|
||||
if Self::monitor_name_from_window(window.hwnd())? != monitor_name {
|
||||
windows_on_other_monitors.push(window.hwnd().0);
|
||||
if Self::monitor_name_from_window(window.hwnd)? != monitor_name {
|
||||
windows_on_other_monitors.push(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,32 +363,34 @@ impl WindowsApi {
|
||||
unsafe { AllowSetForegroundWindow(process_id) }.process()
|
||||
}
|
||||
|
||||
pub fn monitor_from_window(hwnd: HWND) -> isize {
|
||||
pub fn monitor_from_window(hwnd: isize) -> isize {
|
||||
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
|
||||
unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0
|
||||
unsafe { MonitorFromWindow(HWND(as_ptr!(hwnd)), MONITOR_DEFAULTTONEAREST) }.0 as isize
|
||||
}
|
||||
|
||||
pub fn monitor_name_from_window(hwnd: HWND) -> Result<String> {
|
||||
pub fn monitor_name_from_window(hwnd: isize) -> Result<String> {
|
||||
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
|
||||
Ok(
|
||||
Self::monitor(unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }.0)?
|
||||
.name()
|
||||
.to_string(),
|
||||
)
|
||||
Ok(Self::monitor(
|
||||
unsafe { MonitorFromWindow(HWND(as_ptr!(hwnd)), MONITOR_DEFAULTTONEAREST) }.0 as isize,
|
||||
)?
|
||||
.name()
|
||||
.to_string())
|
||||
}
|
||||
|
||||
pub fn monitor_from_point(point: POINT) -> isize {
|
||||
// MONITOR_DEFAULTTONEAREST ensures that the return value will never be NULL
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
|
||||
unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0
|
||||
unsafe { MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST) }.0 as isize
|
||||
}
|
||||
|
||||
/// 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<()> {
|
||||
pub fn position_window(hwnd: isize, layout: &Rect, top: bool) -> Result<()> {
|
||||
let hwnd = HWND(as_ptr!(hwnd));
|
||||
|
||||
let mut flags = SetWindowPosition::NO_ACTIVATE
|
||||
| SetWindowPosition::NO_SEND_CHANGING
|
||||
| SetWindowPosition::NO_COPY_BITS
|
||||
@@ -393,22 +426,32 @@ impl WindowsApi {
|
||||
Self::set_window_pos(hwnd, &rect, HWND_TOP, flags.bits())
|
||||
}
|
||||
|
||||
pub fn bring_window_to_top(hwnd: HWND) -> Result<()> {
|
||||
unsafe { BringWindowToTop(hwnd) }.process()
|
||||
pub fn bring_window_to_top(hwnd: isize) -> Result<()> {
|
||||
unsafe { BringWindowToTop(HWND(as_ptr!(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<()> {
|
||||
pub fn raise_window(hwnd: isize) -> Result<()> {
|
||||
let flags = SetWindowPosition::NO_MOVE | SetWindowPosition::NO_ACTIVATE;
|
||||
|
||||
let position = HWND_TOP;
|
||||
Self::set_window_pos(hwnd, &Rect::default(), position, flags.bits())
|
||||
Self::set_window_pos(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
&Rect::default(),
|
||||
position,
|
||||
flags.bits(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_border_pos(hwnd: HWND, layout: &Rect, position: HWND) -> Result<()> {
|
||||
pub fn set_border_pos(hwnd: isize, layout: &Rect, position: isize) -> Result<()> {
|
||||
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
|
||||
Self::set_window_pos(hwnd, layout, position, flags.bits())
|
||||
Self::set_window_pos(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
layout,
|
||||
HWND(as_ptr!(position)),
|
||||
flags.bits(),
|
||||
)
|
||||
}
|
||||
|
||||
/// set_window_pos calls SetWindowPos without any accounting for Window decorations.
|
||||
@@ -427,13 +470,29 @@ impl WindowsApi {
|
||||
.process()
|
||||
}
|
||||
|
||||
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) };
|
||||
pub fn move_window(hwnd: isize, layout: &Rect, repaint: bool) -> Result<()> {
|
||||
let hwnd = HWND(as_ptr!(hwnd));
|
||||
|
||||
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,
|
||||
};
|
||||
unsafe { MoveWindow(hwnd, rect.left, rect.top, rect.right, rect.bottom, repaint) }.process()
|
||||
}
|
||||
|
||||
pub fn minimize_window(hwnd: HWND) {
|
||||
pub fn show_window(hwnd: isize, 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
|
||||
// TODO: error handling
|
||||
unsafe {
|
||||
let _ = ShowWindow(HWND(as_ptr!(hwnd)), command);
|
||||
};
|
||||
}
|
||||
|
||||
pub fn minimize_window(hwnd: isize) {
|
||||
Self::show_window(hwnd, SW_MINIMIZE);
|
||||
}
|
||||
|
||||
@@ -441,26 +500,26 @@ impl WindowsApi {
|
||||
unsafe { PostMessageW(hwnd, message, wparam, lparam) }.process()
|
||||
}
|
||||
|
||||
pub fn close_window(hwnd: HWND) -> Result<()> {
|
||||
match Self::post_message(hwnd, WM_CLOSE, WPARAM(0), LPARAM(0)) {
|
||||
pub fn close_window(hwnd: isize) -> Result<()> {
|
||||
match Self::post_message(HWND(as_ptr!(hwnd)), WM_CLOSE, WPARAM(0), LPARAM(0)) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => Err(anyhow!("could not close window")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide_window(hwnd: HWND) {
|
||||
pub fn hide_window(hwnd: isize) {
|
||||
Self::show_window(hwnd, SW_HIDE);
|
||||
}
|
||||
|
||||
pub fn restore_window(hwnd: HWND) {
|
||||
pub fn restore_window(hwnd: isize) {
|
||||
Self::show_window(hwnd, SW_SHOWNOACTIVATE);
|
||||
}
|
||||
|
||||
pub fn unmaximize_window(hwnd: HWND) {
|
||||
pub fn unmaximize_window(hwnd: isize) {
|
||||
Self::show_window(hwnd, SW_NORMAL);
|
||||
}
|
||||
|
||||
pub fn maximize_window(hwnd: HWND) {
|
||||
pub fn maximize_window(hwnd: isize) {
|
||||
Self::show_window(hwnd, SW_MAXIMIZE);
|
||||
}
|
||||
|
||||
@@ -468,7 +527,7 @@ impl WindowsApi {
|
||||
unsafe { GetForegroundWindow() }.process()
|
||||
}
|
||||
|
||||
pub fn raise_and_focus_window(hwnd: HWND) -> Result<()> {
|
||||
pub fn raise_and_focus_window(hwnd: isize) -> Result<()> {
|
||||
let event = [INPUT {
|
||||
r#type: INPUT_MOUSE,
|
||||
..Default::default()
|
||||
@@ -480,7 +539,7 @@ impl WindowsApi {
|
||||
SendInput(&event, size_of::<INPUT>() as i32);
|
||||
// Error ignored, as the operation is not always necessary.
|
||||
let _ = SetWindowPos(
|
||||
hwnd,
|
||||
HWND(as_ptr!(hwnd)),
|
||||
HWND_TOP,
|
||||
0,
|
||||
0,
|
||||
@@ -489,7 +548,7 @@ impl WindowsApi {
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW,
|
||||
)
|
||||
.process();
|
||||
SetForegroundWindow(hwnd)
|
||||
SetForegroundWindow(HWND(as_ptr!(hwnd)))
|
||||
}
|
||||
.ok()
|
||||
.process()
|
||||
@@ -497,7 +556,7 @@ impl WindowsApi {
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn top_window() -> Result<isize> {
|
||||
unsafe { GetTopWindow(HWND::default()) }.process()
|
||||
unsafe { GetTopWindow(HWND::default())? }.process()
|
||||
}
|
||||
|
||||
pub fn desktop_window() -> Result<isize> {
|
||||
@@ -505,8 +564,8 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn next_window(hwnd: HWND) -> Result<isize> {
|
||||
unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.process()
|
||||
pub fn next_window(hwnd: isize) -> Result<isize> {
|
||||
unsafe { GetWindow(HWND(as_ptr!(hwnd)), GW_HWNDNEXT)? }.process()
|
||||
}
|
||||
|
||||
pub fn alt_tab_windows() -> Result<Vec<Window>> {
|
||||
@@ -525,17 +584,17 @@ impl WindowsApi {
|
||||
let mut next_hwnd = hwnd;
|
||||
|
||||
while next_hwnd != 0 {
|
||||
if Self::is_window_visible(HWND(next_hwnd)) {
|
||||
if Self::is_window_visible(next_hwnd) {
|
||||
return Ok(next_hwnd);
|
||||
}
|
||||
|
||||
next_hwnd = Self::next_window(HWND(next_hwnd))?;
|
||||
next_hwnd = Self::next_window(next_hwnd)?;
|
||||
}
|
||||
|
||||
Err(anyhow!("could not find next window"))
|
||||
}
|
||||
|
||||
pub fn window_rect(hwnd: HWND) -> Result<Rect> {
|
||||
pub fn window_rect(hwnd: isize) -> Result<Rect> {
|
||||
let mut rect = unsafe { std::mem::zeroed() };
|
||||
|
||||
if Self::dwm_get_window_attribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &mut rect).is_ok() {
|
||||
@@ -545,7 +604,7 @@ impl WindowsApi {
|
||||
// Ok(Rect::from(rect).scale(system_scale.try_into()?, window_scale.try_into()?))
|
||||
Ok(Rect::from(rect))
|
||||
} else {
|
||||
unsafe { GetWindowRect(hwnd, &mut rect) }.process()?;
|
||||
unsafe { GetWindowRect(HWND(as_ptr!(hwnd)), &mut rect) }.process()?;
|
||||
Ok(Rect::from(rect))
|
||||
}
|
||||
}
|
||||
@@ -555,7 +614,7 @@ impl WindowsApi {
|
||||
/// 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 window_rect = Self::window_rect(hwnd.0 as isize)?;
|
||||
|
||||
let mut srect = Default::default();
|
||||
unsafe { GetWindowRect(hwnd, &mut srect) }.process()?;
|
||||
@@ -571,7 +630,8 @@ impl WindowsApi {
|
||||
|
||||
pub fn round_rect(hdc: HDC, rect: &Rect, border_radius: i32) {
|
||||
unsafe {
|
||||
RoundRect(
|
||||
// TODO: error handling
|
||||
let _ = RoundRect(
|
||||
hdc,
|
||||
rect.left,
|
||||
rect.top,
|
||||
@@ -584,7 +644,8 @@ impl WindowsApi {
|
||||
}
|
||||
pub fn rectangle(hdc: HDC, rect: &Rect) {
|
||||
unsafe {
|
||||
Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
|
||||
// TODO: error handling
|
||||
let _ = Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
|
||||
}
|
||||
}
|
||||
fn set_cursor_pos(x: i32, y: i32) -> Result<()> {
|
||||
@@ -610,13 +671,16 @@ impl WindowsApi {
|
||||
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
|
||||
}
|
||||
|
||||
pub fn window_thread_process_id(hwnd: HWND) -> (u32, u32) {
|
||||
pub fn window_thread_process_id(hwnd: isize) -> (u32, u32) {
|
||||
let mut process_id: u32 = 0;
|
||||
|
||||
// Behaviour is undefined if an invalid HWND is given
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowthreadprocessid
|
||||
let thread_id = unsafe {
|
||||
GetWindowThreadProcessId(hwnd, Option::from(std::ptr::addr_of_mut!(process_id)))
|
||||
GetWindowThreadProcessId(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
Option::from(std::ptr::addr_of_mut!(process_id)),
|
||||
)
|
||||
};
|
||||
|
||||
(process_id, thread_id)
|
||||
@@ -639,7 +703,7 @@ impl WindowsApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn set_window_long_ptr_w(
|
||||
hwnd: HWND,
|
||||
index: WINDOW_LONG_PTR_INDEX,
|
||||
@@ -651,14 +715,39 @@ impl WindowsApi {
|
||||
.map(|_| {})
|
||||
}
|
||||
|
||||
pub fn gwl_style(hwnd: HWND) -> Result<isize> {
|
||||
Self::window_long_ptr_w(hwnd, GWL_STYLE)
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
fn set_window_long_ptr_w(
|
||||
hwnd: HWND,
|
||||
index: WINDOW_LONG_PTR_INDEX,
|
||||
new_value: i32,
|
||||
) -> Result<()> {
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
SetWindowLongPtrW(hwnd, index, new_value)
|
||||
}))
|
||||
.map(|_| {})
|
||||
}
|
||||
|
||||
pub fn gwl_ex_style(hwnd: HWND) -> Result<isize> {
|
||||
Self::window_long_ptr_w(hwnd, GWL_EXSTYLE)
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
pub fn gwl_style(hwnd: isize) -> Result<isize> {
|
||||
Self::window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_STYLE)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
pub fn gwl_style(hwnd: isize) -> Result<i32> {
|
||||
Self::window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_STYLE)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
pub fn gwl_ex_style(hwnd: isize) -> Result<isize> {
|
||||
Self::window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_EXSTYLE)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
pub fn gwl_ex_style(hwnd: isize) -> Result<i32> {
|
||||
Self::window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_EXSTYLE)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> Result<isize> {
|
||||
// Can return 0, which does not always mean that an error has occurred
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
|
||||
@@ -667,19 +756,38 @@ impl WindowsApi {
|
||||
}))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn update_style(hwnd: HWND, new_value: isize) -> Result<()> {
|
||||
Self::set_window_long_ptr_w(hwnd, GWL_STYLE, new_value)
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> Result<i32> {
|
||||
// Can return 0, which does not always mean that an error has occurred
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindowLongPtrW(hwnd, index)
|
||||
}))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn update_ex_style(hwnd: HWND, new_value: isize) -> Result<()> {
|
||||
Self::set_window_long_ptr_w(hwnd, GWL_EXSTYLE, new_value)
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
pub fn update_style(hwnd: isize, new_value: isize) -> Result<()> {
|
||||
Self::set_window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_STYLE, new_value)
|
||||
}
|
||||
|
||||
pub fn window_text_w(hwnd: HWND) -> Result<String> {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
pub fn update_style(hwnd: isize, new_value: i32) -> Result<()> {
|
||||
Self::set_window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_STYLE, new_value)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
pub fn update_ex_style(hwnd: isize, new_value: isize) -> Result<()> {
|
||||
Self::set_window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_EXSTYLE, new_value)
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
pub fn update_ex_style(hwnd: isize, new_value: i32) -> Result<()> {
|
||||
Self::set_window_long_ptr_w(HWND(as_ptr!(hwnd)), GWL_EXSTYLE, new_value)
|
||||
}
|
||||
|
||||
pub fn window_text_w(hwnd: isize) -> Result<String> {
|
||||
let mut text: [u16; 512] = [0; 512];
|
||||
match WindowsResult::from(unsafe { GetWindowTextW(hwnd, &mut text) }) {
|
||||
match WindowsResult::from(unsafe { GetWindowTextW(HWND(as_ptr!(hwnd)), &mut text) }) {
|
||||
WindowsResult::Ok(len) => {
|
||||
let length = usize::try_from(len)?;
|
||||
Ok(String::from_utf16(&text[..length])?)
|
||||
@@ -725,25 +833,25 @@ impl WindowsApi {
|
||||
.to_string())
|
||||
}
|
||||
|
||||
pub fn real_window_class_w(hwnd: HWND) -> Result<String> {
|
||||
pub fn real_window_class_w(hwnd: isize) -> Result<String> {
|
||||
const BUF_SIZE: usize = 512;
|
||||
let mut class: [u16; BUF_SIZE] = [0; BUF_SIZE];
|
||||
|
||||
let len = Result::from(WindowsResult::from(unsafe {
|
||||
RealGetWindowClassW(hwnd, &mut class)
|
||||
RealGetWindowClassW(HWND(as_ptr!(hwnd)), &mut class)
|
||||
}))?;
|
||||
|
||||
Ok(String::from_utf16(&class[0..len as usize])?)
|
||||
}
|
||||
|
||||
pub fn dwm_get_window_attribute<T>(
|
||||
hwnd: HWND,
|
||||
hwnd: isize,
|
||||
attribute: DWMWINDOWATTRIBUTE,
|
||||
value: &mut T,
|
||||
) -> Result<()> {
|
||||
unsafe {
|
||||
DwmGetWindowAttribute(
|
||||
hwnd,
|
||||
HWND(as_ptr!(hwnd)),
|
||||
attribute,
|
||||
(value as *mut T).cast(),
|
||||
u32::try_from(std::mem::size_of::<T>())?,
|
||||
@@ -753,7 +861,7 @@ impl WindowsApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_window_cloaked(hwnd: HWND) -> Result<bool> {
|
||||
pub fn is_window_cloaked(hwnd: isize) -> Result<bool> {
|
||||
let mut cloaked: u32 = 0;
|
||||
Self::dwm_get_window_attribute(hwnd, DWMWA_CLOAKED, &mut cloaked)?;
|
||||
|
||||
@@ -763,20 +871,20 @@ impl WindowsApi {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn is_window(hwnd: HWND) -> bool {
|
||||
unsafe { IsWindow(hwnd) }.into()
|
||||
pub fn is_window(hwnd: isize) -> bool {
|
||||
unsafe { IsWindow(HWND(as_ptr!(hwnd))) }.into()
|
||||
}
|
||||
|
||||
pub fn is_window_visible(hwnd: HWND) -> bool {
|
||||
unsafe { IsWindowVisible(hwnd) }.into()
|
||||
pub fn is_window_visible(hwnd: isize) -> bool {
|
||||
unsafe { IsWindowVisible(HWND(as_ptr!(hwnd))) }.into()
|
||||
}
|
||||
|
||||
pub fn is_iconic(hwnd: HWND) -> bool {
|
||||
unsafe { IsIconic(hwnd) }.into()
|
||||
pub fn is_iconic(hwnd: isize) -> bool {
|
||||
unsafe { IsIconic(HWND(as_ptr!(hwnd))) }.into()
|
||||
}
|
||||
|
||||
pub fn is_zoomed(hwnd: HWND) -> bool {
|
||||
unsafe { IsZoomed(hwnd) }.into()
|
||||
pub fn is_zoomed(hwnd: isize) -> bool {
|
||||
unsafe { IsZoomed(HWND(as_ptr!(hwnd))) }.into()
|
||||
}
|
||||
|
||||
pub fn monitor_info_w(hmonitor: HMONITOR) -> Result<MONITORINFOEXW> {
|
||||
@@ -793,11 +901,17 @@ impl WindowsApi {
|
||||
for display in win32_display_data::connected_displays_all().flatten() {
|
||||
if display.hmonitor == hmonitor {
|
||||
let path = display.device_path;
|
||||
let mut split: Vec<_> = path.split('#').collect();
|
||||
split.remove(0);
|
||||
split.remove(split.len() - 1);
|
||||
let device = split[0].to_string();
|
||||
let device_id = split.join("-");
|
||||
|
||||
let (device, device_id) = if path.is_empty() {
|
||||
(String::from("UNKNOWN"), String::from("UNKNOWN"))
|
||||
} else {
|
||||
let mut split: Vec<_> = path.split('#').collect();
|
||||
split.remove(0);
|
||||
split.remove(split.len() - 1);
|
||||
let device = split[0].to_string();
|
||||
let device_id = split.join("-");
|
||||
(device, device_id)
|
||||
};
|
||||
|
||||
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
|
||||
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
|
||||
@@ -922,7 +1036,7 @@ impl WindowsApi {
|
||||
|
||||
unsafe {
|
||||
GetDpiForMonitor(
|
||||
HMONITOR(hmonitor),
|
||||
HMONITOR(as_ptr!(hmonitor)),
|
||||
MDT_EFFECTIVE_DPI,
|
||||
std::ptr::addr_of_mut!(dpi_x),
|
||||
std::ptr::addr_of_mut!(dpi_y),
|
||||
@@ -946,7 +1060,7 @@ impl WindowsApi {
|
||||
|
||||
unsafe {
|
||||
DwmSetWindowAttribute(
|
||||
HWND(hwnd),
|
||||
HWND(as_ptr!(hwnd)),
|
||||
DWMWA_WINDOW_CORNER_PREFERENCE,
|
||||
std::ptr::addr_of!(round).cast(),
|
||||
4,
|
||||
@@ -955,7 +1069,20 @@ impl WindowsApi {
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn create_border_window(name: PCWSTR, instance: HMODULE) -> Result<isize> {
|
||||
pub fn set_window_accent(hwnd: isize, color: Option<u32>) -> Result<()> {
|
||||
let col_ref = COLORREF(color.unwrap_or(DWMWA_COLOR_NONE));
|
||||
unsafe {
|
||||
DwmSetWindowAttribute(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
DWMWA_BORDER_COLOR,
|
||||
std::ptr::addr_of!(col_ref).cast(),
|
||||
4,
|
||||
)
|
||||
}
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn create_border_window(name: PCWSTR, instance: isize) -> Result<isize> {
|
||||
unsafe {
|
||||
let hwnd = CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
|
||||
@@ -968,9 +1095,9 @@ impl WindowsApi {
|
||||
CW_USEDEFAULT,
|
||||
None,
|
||||
None,
|
||||
instance,
|
||||
HINSTANCE(as_ptr!(instance)),
|
||||
None,
|
||||
);
|
||||
)?;
|
||||
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
|
||||
|
||||
@@ -979,16 +1106,21 @@ impl WindowsApi {
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn set_transparent(hwnd: HWND, alpha: u8) -> Result<()> {
|
||||
pub fn set_transparent(hwnd: isize, alpha: u8) -> Result<()> {
|
||||
unsafe {
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(-1i32 as u32), alpha, LWA_ALPHA)?;
|
||||
SetLayeredWindowAttributes(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
COLORREF(-1i32 as u32),
|
||||
alpha,
|
||||
LWA_ALPHA,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_hidden_window(name: PCWSTR, instance: HMODULE) -> Result<isize> {
|
||||
pub fn create_hidden_window(name: PCWSTR, instance: isize) -> Result<isize> {
|
||||
unsafe {
|
||||
CreateWindowExW(
|
||||
WS_EX_NOACTIVATE,
|
||||
@@ -1001,13 +1133,18 @@ impl WindowsApi {
|
||||
CW_USEDEFAULT,
|
||||
None,
|
||||
None,
|
||||
instance,
|
||||
HINSTANCE(as_ptr!(instance)),
|
||||
None,
|
||||
)
|
||||
)?
|
||||
}
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn invalidate_rect(hwnd: isize, rect: Option<&Rect>, erase: bool) -> bool {
|
||||
let rect = rect.map(|rect| &rect.rect() as *const RECT);
|
||||
unsafe { InvalidateRect(HWND(as_ptr!(hwnd)), rect, erase) }.as_bool()
|
||||
}
|
||||
|
||||
pub fn alt_is_pressed() -> bool {
|
||||
let state = unsafe { GetKeyState(i32::from(VK_MENU.0)) };
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
@@ -1059,6 +1196,6 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn wts_register_session_notification(hwnd: isize) -> Result<()> {
|
||||
unsafe { WTSRegisterSessionNotification(HWND(hwnd), 1) }.process()
|
||||
unsafe { WTSRegisterSessionNotification(HWND(as_ptr!(hwnd)), 1) }.process()
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user