mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-11 07:47:39 +01:00
Compare commits
131 Commits
feature/aa
...
feature/ic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0099cd754 | ||
|
|
e9bb6b43d6 | ||
|
|
79eda30f48 | ||
|
|
692da90890 | ||
|
|
4babf336ec | ||
|
|
53a83eedb5 | ||
|
|
e1bbd3c1f5 | ||
|
|
2c08fbe8f6 | ||
|
|
cced2a4433 | ||
|
|
d93b6fa1b3 | ||
|
|
99353b8064 | ||
|
|
c64a42bca5 | ||
|
|
5ab5ec4f3a | ||
|
|
ad08585faf | ||
|
|
eb8a988841 | ||
|
|
0e2a55b300 | ||
|
|
eda91dcd1d | ||
|
|
0c6317a27b | ||
|
|
5c81a8c9e2 | ||
|
|
a4128b7276 | ||
|
|
73a4df884c | ||
|
|
32a234317c | ||
|
|
0dc6780da6 | ||
|
|
f089d3e59b | ||
|
|
5dbf0f1b89 | ||
|
|
d393f8fe77 | ||
|
|
c3769e7881 | ||
|
|
3c0b12f9af | ||
|
|
804faef229 | ||
|
|
7bf1521363 | ||
|
|
b49e634b65 | ||
|
|
be0671be6d | ||
|
|
10539a4bab | ||
|
|
9463c75f12 | ||
|
|
c31c5dc69d | ||
|
|
6c07863b81 | ||
|
|
40c55dec39 | ||
|
|
5cc2d9d469 | ||
|
|
91b255280a | ||
|
|
9bd1073a83 | ||
|
|
53c1990442 | ||
|
|
9d6173ecbb | ||
|
|
830da89529 | ||
|
|
f59d7a51f1 | ||
|
|
1470c63cfe | ||
|
|
64382b18c1 | ||
|
|
26f90cc9ee | ||
|
|
192af6751b | ||
|
|
4f306e5bfd | ||
|
|
ede0b23bb4 | ||
|
|
e6b5b78857 | ||
|
|
440d78e8f4 | ||
|
|
280ca0ffcd | ||
|
|
f227bd0fef | ||
|
|
3781c8ea41 | ||
|
|
33800903f7 | ||
|
|
bb31e7155d | ||
|
|
40b32332ae | ||
|
|
8743cdd292 | ||
|
|
01367f59e2 | ||
|
|
e6ddccc5f6 | ||
|
|
b22ec90438 | ||
|
|
46b81e4372 | ||
|
|
6f00c527a4 | ||
|
|
3ad4090df8 | ||
|
|
449ccac645 | ||
|
|
1d00196240 | ||
|
|
639ebd0b3d | ||
|
|
e22eafbc8e | ||
|
|
46c2ad512b | ||
|
|
3de96609bb | ||
|
|
9c09284b0f | ||
|
|
b14c0d07a2 | ||
|
|
3615451f41 | ||
|
|
6893f39dd9 | ||
|
|
041ef5731c | ||
|
|
779c12bc6a | ||
|
|
0e48370b73 | ||
|
|
8de92ec32a | ||
|
|
ac243c6992 | ||
|
|
d001d8a7a6 | ||
|
|
219fa8e14f | ||
|
|
e4e94fd1a6 | ||
|
|
e707a14b8a | ||
|
|
818ec1c63b | ||
|
|
a10bb467e5 | ||
|
|
4fd60bbff3 | ||
|
|
cc196db046 | ||
|
|
7f0b54c35e | ||
|
|
b1726af2eb | ||
|
|
fd8cd4bb01 | ||
|
|
172988ed81 | ||
|
|
36e3eaad36 | ||
|
|
0f022d47df | ||
|
|
dc1eb8ff50 | ||
|
|
166f505aba | ||
|
|
d55d356b37 | ||
|
|
9a3dbccc89 | ||
|
|
24b43a154c | ||
|
|
6a09ec4b87 | ||
|
|
374cd2c6d5 | ||
|
|
0f385d6245 | ||
|
|
5503323695 | ||
|
|
aa9f50fd5c | ||
|
|
3a6ae01b12 | ||
|
|
720089587b | ||
|
|
852d1f9eb0 | ||
|
|
ed5b0f9120 | ||
|
|
7f7b8c7c05 | ||
|
|
4198f6fabc | ||
|
|
4acd408b88 | ||
|
|
73d928771d | ||
|
|
d194afe6e6 | ||
|
|
72f4ed0575 | ||
|
|
82ff69e337 | ||
|
|
583d8b1e4c | ||
|
|
2f1f1b160c | ||
|
|
bcefea8a1a | ||
|
|
223404a1aa | ||
|
|
7d032db507 | ||
|
|
09afad624f | ||
|
|
903b6af507 | ||
|
|
1644fcf6f2 | ||
|
|
b31e8911ce | ||
|
|
298fbafe00 | ||
|
|
e46a1a6117 | ||
|
|
34929f32a7 | ||
|
|
1ef7a09163 | ||
|
|
c0c3c81d69 | ||
|
|
7276dc2309 | ||
|
|
58d3086615 |
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -3,6 +3,14 @@ description: File a bug report
|
||||
labels: [bug]
|
||||
title: "[BUG]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please **do not** open an issue for applications with invisible windows leaving ghost tiles.
|
||||
|
||||
You can run `komorebic visible-windows` when the ghost tile is present on your workspace to retrieve the invisible window's exe, class name and title, and then use that to [ignore the window](https://lgug2z.github.io/komorebi/common-workflows/ignore-windows.html) responsible for the ghost tile.
|
||||
|
||||
If it is not possible to uniquely identify the invisible window resulting in a ghost tile through a mixture of exe, title and class identifiers , then this is not a bug with komorebi but a bug with the application you are using, and should open an issue with the developer(s) of that application.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Feature request
|
||||
description: Suggest a new feature
|
||||
description: Suggest a new feature (Sponsors only)
|
||||
labels: [enhancement]
|
||||
title: "[FEAT]: "
|
||||
body:
|
||||
|
||||
7
.github/pull_request_template.md
vendored
Normal file
7
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<!--
|
||||
Please follow the Conventional Commits specification.
|
||||
|
||||
If you need to update your PR with changes from `master`, please run `git rebase master`.
|
||||
|
||||
By opening this PR, you confirm that you have read and understood this project's `CONTRIBUTING.md`.
|
||||
-->
|
||||
125
.github/workflows/sponsor-check.yaml
vendored
Normal file
125
.github/workflows/sponsor-check.yaml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
name: Feature Request Sponsor Check
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_username:
|
||||
description: "Test username to check sponsorship for"
|
||||
required: true
|
||||
default: "octocat"
|
||||
test_title:
|
||||
description: "Test issue title"
|
||||
required: true
|
||||
default: "[FEAT] Test Feature Request"
|
||||
test_sponsor_platform:
|
||||
description: "Selected sponsor platform"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- "GitHub Sponsors"
|
||||
- "Ko-fi"
|
||||
- "Discord"
|
||||
- "YouTube"
|
||||
|
||||
jobs:
|
||||
check-sponsor:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'workflow_dispatch') || (github.event_name == 'issues' &&
|
||||
startsWith(github.event.issue.title, '[FEAT]') &&
|
||||
github.event.issue.user.login != 'LGUG2Z' &&
|
||||
fromJSON(github.event.issue.body).Sponsors == 'GitHub Sponsors')
|
||||
|
||||
steps:
|
||||
- name: Get Issue Details
|
||||
id: issue-details
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "username=${{ github.event.inputs.test_username }}" >> $GITHUB_OUTPUT
|
||||
echo "title=${{ github.event.inputs.test_title }}" >> $GITHUB_OUTPUT
|
||||
echo "sponsor_platform=${{ github.event.inputs.test_sponsor_platform }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "username=${{ github.event.issue.user.login }}" >> $GITHUB_OUTPUT
|
||||
echo "title=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT
|
||||
echo "sponsor_platform=$(jq -r '.Sponsors' <<< '${{ github.event.issue.body }}')" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Get Sponsorship Status
|
||||
id: sponsorship
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.PAT }}
|
||||
script: |
|
||||
const username = '${{ steps.issue-details.outputs.username }}';
|
||||
const sponsorPlatform = '${{ steps.issue-details.outputs.sponsor_platform }}';
|
||||
|
||||
if (sponsorPlatform !== 'GitHub Sponsors') {
|
||||
console.log('Sponsor platform is not GitHub Sponsors, skipping check');
|
||||
return true;
|
||||
}
|
||||
|
||||
const sponsorshipQuery = `query($user: String!) {
|
||||
user(login: $user) {
|
||||
... on Sponsorable {
|
||||
sponsorshipForViewerAsSponsorable {
|
||||
tier {
|
||||
name
|
||||
monthlyPriceInDollars
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
try {
|
||||
const result = await github.graphql(sponsorshipQuery, {
|
||||
user: username
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
const sponsorship = result.user.sponsorshipForViewerAsSponsorable;
|
||||
console.log(sponsorship);
|
||||
const amount = sponsorship?.tier?.monthlyPriceInDollars || 0;
|
||||
|
||||
console.log(`Sponsorship amount for ${username}: $${amount}/month`);
|
||||
return amount >= 5;
|
||||
} catch (error) {
|
||||
console.log(`Error checking sponsorship: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
- name: Print Test Results
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Test Results for ${{ steps.issue-details.outputs.username }}:"
|
||||
echo "Title: ${{ steps.issue-details.outputs.title }}"
|
||||
echo "Platform: ${{ steps.issue-details.outputs.sponsor_platform }}"
|
||||
echo "Would close issue: ${{ steps.sponsorship.outputs.result == 'false' }}"
|
||||
|
||||
- name: Close Issue If Not Sponsor
|
||||
if: |
|
||||
github.event_name == 'issues' &&
|
||||
steps.sponsorship.outputs.result == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issueNumber = context.issue.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body: 'Thank you for your feature request! This repository requires a GitHub sponsorship of at least $5/month to submit feature requests. Please consider becoming a sponsor at https://github.com/sponsors/LGUG2Z'
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed'
|
||||
});
|
||||
16
.github/workflows/windows.yaml
vendored
16
.github/workflows/windows.yaml
vendored
@@ -15,6 +15,7 @@ on:
|
||||
- v*
|
||||
schedule:
|
||||
- cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -43,9 +44,10 @@ jobs:
|
||||
with:
|
||||
cache-on-failure: "true"
|
||||
cache-all-crates: "true"
|
||||
key: ${{ matrix.platform.target }}
|
||||
- run: cargo +nightly fmt --check
|
||||
- run: cargo clippy
|
||||
- uses: houseabsolute/actions-rust-cross@v0
|
||||
- uses: houseabsolute/actions-rust-cross@v1
|
||||
with:
|
||||
command: "build"
|
||||
target: ${{ matrix.platform.target }}
|
||||
@@ -66,7 +68,7 @@ jobs:
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
permissions: write-all
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
|
||||
if: ${{ github.ref == 'refs/heads/master' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' ) }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
@@ -84,6 +86,10 @@ jobs:
|
||||
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
|
||||
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: "true"
|
||||
cache-all-crates: "true"
|
||||
- shell: bash
|
||||
run: |
|
||||
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
|
||||
@@ -98,7 +104,7 @@ jobs:
|
||||
--target $GITHUB_SHA \
|
||||
--prerelease \
|
||||
--title "komorebi nightly (${GITHUB_SHA})" \
|
||||
--notes-file CHANGELOG.md
|
||||
--notes-file CHANGELOG.md \
|
||||
komorebi-nightly-x86_64-pc-windows-msvc.zip \
|
||||
komorebi-nightly-x86_64.msi \
|
||||
komorebi-nightly-aarch64-pc-windows-msvc.zip \
|
||||
@@ -160,7 +166,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
TAG=${{ github.ref_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- run: |
|
||||
@@ -193,7 +199,7 @@ jobs:
|
||||
needs: release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
- uses: vedantmgoyal2009/winget-releaser@main
|
||||
with:
|
||||
identifier: LGUG2Z.komorebi
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,4 +4,5 @@
|
||||
CHANGELOG.md
|
||||
dummy.go
|
||||
komorebic/applications.yaml
|
||||
komorebic/applications.json
|
||||
/.vs
|
||||
|
||||
1328
Cargo.lock
generated
1328
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -17,8 +17,8 @@ chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.29"
|
||||
egui_extras = "0.29"
|
||||
eframe = "0.30"
|
||||
egui_extras = "0.30"
|
||||
dirs = "5"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
@@ -38,17 +38,21 @@ windows-implement = { version = "0.58" }
|
||||
windows-interface = { version = "0.58" }
|
||||
windows-core = { version = "0.58" }
|
||||
shadow-rs = "0.35"
|
||||
which = "6"
|
||||
which = "7"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.58"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
|
||||
117
README.md
117
README.md
@@ -29,6 +29,8 @@ Tiling Window Management for Windows.
|
||||
|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
|
||||
[Desktop Window
|
||||
Manager](https://docs.microsoft.com/en-us/windows/win32/dwm/dwm-overview) in
|
||||
@@ -50,6 +52,8 @@ _komorebi_, [common workflows](https://lgug2z.github.io/komorebi/common-workflow
|
||||
[configuration schema reference](https://komorebi.lgug2z.com/schema) and a
|
||||
complete [CLI reference](https://lgug2z.github.io/komorebi/cli/quickstart.html).
|
||||
|
||||
## Community
|
||||
|
||||
There is a [Discord server](https://discord.gg/mGkn66PHkx) available for
|
||||
_komorebi_-related discussion, help, troubleshooting etc. If you have any
|
||||
specific feature requests or bugs to report, please create an issue in this
|
||||
@@ -57,24 +61,62 @@ repository.
|
||||
|
||||
There is a [YouTube
|
||||
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_ development videos, feature previews and release overviews. Subscribing
|
||||
to the channel (which is monetized as part of the YouTube Partner Program) and
|
||||
watching videos is a really simple and passive way to contribute financially to
|
||||
the development and maintenance of _komorebi_.
|
||||
|
||||
_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
|
||||
There is an [Awesome List](https://github.com/LGUG2Z/awesome-komorebi) which
|
||||
showcases the many awesome projects that exist in the _komorebi_ ecosystem.
|
||||
|
||||
## Licensing for Personal Use
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 1.0.0
|
||||
license](https://github.com/LGUG2Z/komorebi-license), 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` for
|
||||
personal use other than redistribution, or distribution of new works (i.e.
|
||||
hard-forks) based on the software.
|
||||
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended either
|
||||
for personal use or for integration back upstream via pull requests.
|
||||
|
||||
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does
|
||||
not permit any kind of commercial use (i.e. using `komorebi` at work).
|
||||
|
||||
## Sponsorship for Personal Use
|
||||
|
||||
_komorebi_ is a free and educational source project, and one that encourages you
|
||||
to make charitable donations if you find the software to be useful and have the
|
||||
financial means.
|
||||
|
||||
I encourage you to make a charitable donation to the [Palestine Children's
|
||||
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) before you
|
||||
consider sponsoring me on GitHub.
|
||||
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or to contribute
|
||||
to a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring
|
||||
me on GitHub.
|
||||
|
||||
[GitHub Sponsors is enabled for this
|
||||
project](https://github.com/sponsors/LGUG2Z). Unfortunately I don't have
|
||||
anything specific to offer besides my gratitude and shout outs at the end of
|
||||
_komorebi_ live development videos and tutorials.
|
||||
project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on
|
||||
the Discord server, get shout outs at the end of _komorebi_-related videos on
|
||||
YouTube, and gain the ability to submit feature requests on the issue tracker.
|
||||
|
||||
If you would like to tip or sponsor the project but are unable to use GitHub
|
||||
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z).
|
||||
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
|
||||
make an anonymous Bitcoin donation to `bc1qv73wzspc77k46uty4vp85x8sdp24mphvm58f6q`.
|
||||
|
||||
## Licensing for Commercial Use
|
||||
|
||||
A dedicated Individual Commercial Use License is available for those who want to
|
||||
use `komorebi` at work.
|
||||
|
||||
The Individual Commerical Use License adds “Commercial Use” as a “Permitted Use”
|
||||
for the licensed individual only, for the duration of a valid paid license
|
||||
subscription only. All provisions and restrictions enumerated in the [Komorebi
|
||||
License](https://github.com/LGUG2Z/komorebi-license) continue to apply.
|
||||
|
||||
More information, pricing and purchase links for Individual Commercial Use
|
||||
Licenses [can be found here](https://lgug2z.com/software/komorebi).
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -96,7 +138,6 @@ video will answer the majority of your questions.
|
||||
|
||||
[](https://www.youtube.com/watch?v=0LCbS_gm0RA)
|
||||
|
||||
|
||||
# Demonstrations
|
||||
|
||||
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
|
||||
@@ -105,7 +146,6 @@ _komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komo
|
||||
|
||||
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
|
||||
@@ -123,7 +163,11 @@ https://user-images.githubusercontent.com/13164844/163496414-a9cde3d1-b8a7-4a7a-
|
||||
|
||||
# Contribution Guidelines
|
||||
|
||||
If you would like to contribute to `komorebi` please take the time to carefully read the guidelines below.
|
||||
If you would like to contribute to `komorebi` please take the time to carefully
|
||||
read the guidelines below.
|
||||
|
||||
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
|
||||
code contributions to `komorebi` are licensed.
|
||||
|
||||
## Commit hygiene
|
||||
|
||||
@@ -133,8 +177,8 @@ If you would like to contribute to `komorebi` please take the time to carefully
|
||||
- Use `git cz` with
|
||||
the [Commitizen CLI](https://github.com/commitizen/cz-cli#conventional-commit-messages-as-a-global-utility) to prepare
|
||||
commit messages
|
||||
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for the
|
||||
changes being committed
|
||||
- Provide **at least** one short sentence or paragraph in your commit message body to describe your thought process for
|
||||
the changes being committed
|
||||
|
||||
## PRs should contain only a single feature or bug fix
|
||||
|
||||
@@ -173,7 +217,8 @@ This includes but is not limited to:
|
||||
|
||||
- All `komorebic` commands
|
||||
- The `komorebi.json` schema
|
||||
- The [`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
- The [
|
||||
`komorebi-application-specific-configuration`](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
schema
|
||||
|
||||
No user should ever find that their configuration file has stopped working after upgrading to a new version
|
||||
@@ -189,21 +234,6 @@ ability for users to specify colours in `komorebi.json` in Hex format alongside
|
||||
There is also a process in place for graceful, non-breaking, deprecation of configuration options that are no longer
|
||||
required.
|
||||
|
||||
## License
|
||||
|
||||
`komorebi` is licensed under the [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
|
||||
software.
|
||||
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended
|
||||
either for personal use or for integration back upstream via pull requests.
|
||||
|
||||
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
|
||||
code contributions to `komorebi` are licensed.
|
||||
|
||||
# Development
|
||||
|
||||
If you use IntelliJ, you should enable the following settings to ensure that code generated by macros is recognised by
|
||||
@@ -212,13 +242,13 @@ the IDE for completions and navigation:
|
||||
- Set `Expand declarative macros`
|
||||
to `Use new engine` under "Settings > Langauges & Frameworks > Rust"
|
||||
- Enable the following experimental features:
|
||||
- `org.rust.cargo.evaluate.build.scripts`
|
||||
- `org.rust.macros.proc`
|
||||
- `org.rust.cargo.evaluate.build.scripts`
|
||||
- `org.rust.macros.proc`
|
||||
|
||||
# Logs and Debugging
|
||||
|
||||
Logs from `komorebi` will be appended to `%LOCALAPPDATA%/komorebi/komorebi.log`; this file is never rotated or overwritten, so it will keep
|
||||
growing until it is deleted by the user.
|
||||
Logs from `komorebi` will be appended to `%LOCALAPPDATA%/komorebi/komorebi.log`; this file is never rotated or
|
||||
overwritten, so it will keep growing until it is deleted by the user.
|
||||
|
||||
Whenever running the `komorebic stop` command or sending a Ctrl-C signal to `komorebi` directly, the `komorebi` process
|
||||
ensures that all hidden windows are restored before termination.
|
||||
@@ -359,7 +389,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.29"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.31"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
@@ -434,12 +464,17 @@ programming languages.
|
||||
|
||||
# Appreciations
|
||||
|
||||
- First and foremost, thank you to my wife, both for naming this project and for her patience throughout its never-ending development
|
||||
- First and foremost, thank you to my wife, both for naming this project and for her patience throughout its
|
||||
never-ending development
|
||||
|
||||
- Thank you to [@sitiom](https://github.com/sitiom) for being [an exemplary open source community leader](https://jeezy.substack.com/p/the-open-source-contributions-i-appreciate)
|
||||
- Thank you to [@sitiom](https://github.com/sitiom) for
|
||||
being [an exemplary open source community leader](https://jeezy.substack.com/p/the-open-source-contributions-i-appreciate)
|
||||
|
||||
- Thank you to the developers of [nog](https://github.com/TimUntersberger/nog) who came before me and whose work taught me more than I can ever hope to repay
|
||||
- Thank you to the developers of [nog](https://github.com/TimUntersberger/nog) who came before me and whose work taught
|
||||
me more than I can ever hope to repay
|
||||
|
||||
- Thank you to the developers of [GlazeWM](https://github.com/lars-berger/GlazeWM) for pushing the boundaries of tiling window management on Windows with me and having an excellent spirit of collaboration
|
||||
- Thank you to the developers of [GlazeWM](https://github.com/lars-berger/GlazeWM) for pushing the boundaries of tiling
|
||||
window management on Windows with me and having an excellent spirit of collaboration
|
||||
|
||||
- Thank you to [@Ciantic](https://github.com/Ciantic) for helping me bring the [hidden Virtual Desktops cloaking function](https://github.com/Ciantic/AltTabAccessor/issues/1) to `komorebi`
|
||||
- Thank you to [@Ciantic](https://github.com/Ciantic) for helping me bring
|
||||
the [hidden Virtual Desktops cloaking function](https://github.com/Ciantic/AltTabAccessor/issues/1) to `komorebi`
|
||||
|
||||
@@ -10,9 +10,8 @@ Options:
|
||||
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]
|
||||
[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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# application-specific-configuration-schema
|
||||
|
||||
```
|
||||
Generate a JSON Schema for applications.yaml
|
||||
Generate a JSON Schema for applications.json
|
||||
|
||||
Usage: komorebic.exe application-specific-configuration-schema
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Arguments:
|
||||
Options:
|
||||
-w, --window-kind <WINDOW_KIND>
|
||||
[default: single]
|
||||
[possible values: single, stack, monocle, unfocused]
|
||||
[possible values: single, stack, monocle, unfocused, floating]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
16
docs/cli/convert-app-specific-configuration.md
Normal file
16
docs/cli/convert-app-specific-configuration.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# convert-app-specific-configuration
|
||||
|
||||
```
|
||||
Convert a v1 ASC YAML file to a v2 ASC JSON file
|
||||
|
||||
Usage: komorebic.exe convert-app-specific-configuration <PATH>
|
||||
|
||||
Arguments:
|
||||
<PATH>
|
||||
YAML file from which the application-specific configurations should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -9,9 +9,6 @@ Options:
|
||||
-c, --config <CONFIG>
|
||||
Path to a static configuration JSON file
|
||||
|
||||
-f, --ffm
|
||||
Enable komorebi's custom focus-follows-mouse implementation
|
||||
|
||||
--whkd
|
||||
Enable autostart of whkd
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# fetch-app-specific-configuration
|
||||
|
||||
```
|
||||
Fetch the latest version of applications.yaml from komorebi-application-specific-configuration
|
||||
Fetch the latest version of applications.json from komorebi-application-specific-configuration
|
||||
|
||||
Usage: komorebic.exe fetch-app-specific-configuration
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# focus-follows-mouse
|
||||
|
||||
```
|
||||
Enable or disable focus follows mouse for the operating system
|
||||
|
||||
Usage: komorebic.exe focus-follows-mouse [OPTIONS] <BOOLEAN_STATE>
|
||||
|
||||
Arguments:
|
||||
<BOOLEAN_STATE>
|
||||
[possible values: enable, disable]
|
||||
|
||||
Options:
|
||||
-i, --implementation <IMPLEMENTATION>
|
||||
[default: windows]
|
||||
|
||||
Possible values:
|
||||
- komorebi: A custom FFM implementation (slightly more CPU-intensive)
|
||||
- windows: The native (legacy) Windows FFM implementation
|
||||
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
# format-app-specific-configuration
|
||||
|
||||
```
|
||||
Format a YAML file for use with the 'ahk-app-specific-configuration' command
|
||||
|
||||
Usage: komorebic.exe format-app-specific-configuration <PATH>
|
||||
|
||||
Arguments:
|
||||
<PATH>
|
||||
YAML file from which the application-specific configurations should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,9 +1,9 @@
|
||||
# float-rule
|
||||
# ignore-rule
|
||||
|
||||
```
|
||||
Add a rule to always float the specified application
|
||||
Add a rule to ignore the specified application
|
||||
|
||||
Usage: komorebic.exe float-rule <IDENTIFIER> <ID>
|
||||
Usage: komorebic.exe ignore-rule <IDENTIFIER> <ID>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
@@ -1,16 +0,0 @@
|
||||
# load-custom-layout
|
||||
|
||||
```
|
||||
Load a custom layout from file for the focused workspace
|
||||
|
||||
Usage: komorebic.exe load-custom-layout <PATH>
|
||||
|
||||
Arguments:
|
||||
<PATH>
|
||||
JSON or YAML file from which the custom layout definition should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
# named-workspace-custom-layout-rule
|
||||
|
||||
```
|
||||
Add a dynamic custom layout for the specified workspace
|
||||
|
||||
Usage: komorebic.exe named-workspace-custom-layout-rule <WORKSPACE> <AT_CONTAINER_COUNT> <PATH>
|
||||
|
||||
Arguments:
|
||||
<WORKSPACE>
|
||||
Target workspace name
|
||||
|
||||
<AT_CONTAINER_COUNT>
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<PATH>
|
||||
JSON or YAML file from which the custom layout definition should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
# named-workspace-custom-layout
|
||||
|
||||
```
|
||||
Set a custom layout for the specified workspace
|
||||
|
||||
Usage: komorebic.exe named-workspace-custom-layout <WORKSPACE> <PATH>
|
||||
|
||||
Arguments:
|
||||
<WORKSPACE>
|
||||
Target workspace name
|
||||
|
||||
<PATH>
|
||||
JSON or YAML file from which the custom layout definition should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -6,9 +6,6 @@ Start komorebi.exe as a background process
|
||||
Usage: komorebic.exe start [OPTIONS]
|
||||
|
||||
Options:
|
||||
-f, --ffm
|
||||
Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
|
||||
-c, --config <CONFIG>
|
||||
Path to a static configuration JSON file
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ Options:
|
||||
--whkd
|
||||
Stop whkd if it is running as a background process
|
||||
|
||||
--ahk
|
||||
Stop ahk if it is running as a background process
|
||||
|
||||
--bar
|
||||
Stop komorebi-bar if it is running as a background process
|
||||
|
||||
|
||||
12
docs/cli/toggle-float-override.md
Normal file
12
docs/cli/toggle-float-override.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-float-override
|
||||
|
||||
```
|
||||
Enable or disable float override, which makes it so every new window opens in floating mode
|
||||
|
||||
Usage: komorebic.exe toggle-float-override
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
# toggle-focus-follows-mouse
|
||||
|
||||
```
|
||||
Toggle focus follows mouse for the operating system
|
||||
|
||||
Usage: komorebic.exe toggle-focus-follows-mouse [OPTIONS]
|
||||
|
||||
Options:
|
||||
-i, --implementation <IMPLEMENTATION>
|
||||
[default: windows]
|
||||
|
||||
Possible values:
|
||||
- komorebi: A custom FFM implementation (slightly more CPU-intensive)
|
||||
- windows: The native (legacy) Windows FFM implementation
|
||||
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
```
|
||||
12
docs/cli/toggle-workspace-float-override.md
Normal file
12
docs/cli/toggle-workspace-float-override.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-workspace-float-override
|
||||
|
||||
```
|
||||
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace previously it takes the opposite of the global value
|
||||
|
||||
Usage: komorebic.exe toggle-workspace-float-override
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/toggle-workspace-window-container-behaviour.md
Normal file
12
docs/cli/toggle-workspace-window-container-behaviour.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-workspace-window-container-behaviour
|
||||
|
||||
```
|
||||
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the global value
|
||||
|
||||
Usage: komorebic.exe toggle-workspace-window-container-behaviour
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,25 +0,0 @@
|
||||
# workspace-custom-layout-rule
|
||||
|
||||
```
|
||||
Add a dynamic custom layout for the specified workspace
|
||||
|
||||
Usage: komorebic.exe workspace-custom-layout-rule <MONITOR> <WORKSPACE> <AT_CONTAINER_COUNT> <PATH>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<AT_CONTAINER_COUNT>
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<PATH>
|
||||
JSON or YAML file from which the custom layout definition should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
# workspace-custom-layout
|
||||
|
||||
```
|
||||
Set a custom layout for the specified workspace
|
||||
|
||||
Usage: komorebic.exe workspace-custom-layout <MONITOR> <WORKSPACE> <PATH>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<PATH>
|
||||
JSON or YAML file from which the custom layout definition should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
29
docs/common-workflows/autostart.md
Normal file
29
docs/common-workflows/autostart.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Autostart
|
||||
|
||||
If you would like to autostart `komorebi`, you can use the `komorebic enable-autostart` command to generate a shortcut
|
||||
in the `shell:startup` folder.
|
||||
|
||||
```
|
||||
Generates the komorebi.lnk shortcut in shell:startup to autostart komorebi
|
||||
|
||||
Usage: komorebic.exe enable-autostart [OPTIONS]
|
||||
|
||||
Options:
|
||||
-c, --config <CONFIG>
|
||||
Path to a static configuration JSON file
|
||||
|
||||
-f, --ffm
|
||||
Enable komorebi's custom focus-follows-mouse implementation
|
||||
|
||||
--whkd
|
||||
Enable autostart of whkd
|
||||
|
||||
--ahk
|
||||
Enable autostart of ahk
|
||||
|
||||
--bar
|
||||
Enable autostart of komorebi-bar
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
```
|
||||
@@ -1,70 +0,0 @@
|
||||
# Custom Layouts
|
||||
|
||||
Particularly for users of ultrawide monitors, traditional tiling layouts may
|
||||
not seem like the most efficient use of screen space. If you feel this is the
|
||||
case with any of the default layouts, you are also welcome to create your own
|
||||
custom layouts and save them as JSON or YAML.
|
||||
|
||||
If you're not comfortable writing the layouts directly in JSON or YAML, you can
|
||||
use the [komorebi Custom Layout
|
||||
Generator](https://lgug2z.github.io/komorebi-custom-layout-generator/) to
|
||||
interactively define a custom layout, and then copy the generated JSON content.
|
||||
|
||||
Custom layouts can be loaded on the current workspace or configured for a
|
||||
specific workspace in the `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "personal",
|
||||
"custom_layout": "C:/Users/LGUG2Z/my-custom-layout.json"
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The fundamental building block of a custom _komorebi_ layout is the Column.
|
||||
|
||||
Columns come in three variants:
|
||||
|
||||
- **Primary**: This is where your primary focus will be on the screen most of
|
||||
the time. There must be exactly one Primary Column in any custom layout.
|
||||
Optionally, you can specify the percentage of the screen width that you want
|
||||
the Primary Column to occupy.
|
||||
- **Secondary**: This is an optional column that can either be full height of
|
||||
split horizontally into a fixed number of maximum rows. There can be any
|
||||
number of Secondary Columns in a custom layout.
|
||||
- **Tertiary**: This is the final column where any remaining windows will be
|
||||
split horizontally into rows as they get added.
|
||||
|
||||
If there is only one window on the screen when a custom layout is selected,
|
||||
that window will take up the full work area of the screen.
|
||||
|
||||
If the number of windows is equal to or less than the total number of columns
|
||||
defined in a custom layout, the windows will be arranged in an equal-width
|
||||
columns.
|
||||
|
||||
When the number of windows is greater than the number of columns defined in the
|
||||
custom layout, the windows will begin to be arranged according to the
|
||||
constraints set on the Primary and Secondary columns of the layout.
|
||||
|
||||
Here is an example custom layout that can be used as a starting point for your
|
||||
own:
|
||||
|
||||
```yaml
|
||||
- column: Secondary
|
||||
configuration: !Horizontal 2 # max number of rows
|
||||
- column: Primary
|
||||
configuration: !WidthPercentage 50 # percentage of screen
|
||||
- column: Tertiary
|
||||
configuration: Horizontal
|
||||
```
|
||||
|
||||
<!-- TODO: Record a new video -->
|
||||
|
||||
[](https://www.youtube.com/watch?v=SgmBHKEOcQ4)
|
||||
16
docs/common-workflows/floating-applications.md
Normal file
16
docs/common-workflows/floating-applications.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Floating Windows
|
||||
|
||||
Sometimes you will want a specific application to be managed as a floating window.
|
||||
You can add rules to enforce this behaviour in the `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"floating_applications": [
|
||||
{
|
||||
"kind": "Title",
|
||||
"id": "Media Player",
|
||||
"matching_strategy": "Equals"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,34 +0,0 @@
|
||||
# Focus Follows Mouse
|
||||
|
||||
`komorebi` supports two focus-follows-mouse implementations; the native Windows
|
||||
Xmouse implementation, which treats the desktop, the task bar, and the system
|
||||
tray as windows and switches focus to them eagerly, and a custom `komorebi`
|
||||
implementation, which only considers windows managed by `komorebi` as valid
|
||||
targets to switch focus to when moving the mouse.
|
||||
|
||||
To enable the `komorebi` implementation you must start the process with the
|
||||
`--ffm` flag to explicitly enable the feature. This is because the mouse
|
||||
tracking required for this feature significantly increases the CPU usage of the
|
||||
process (on my machine, it jumps from <1% to ~4~), and this CPU increase
|
||||
persists regardless of whether focus-follows-mouse is enabled or disabled at
|
||||
any given time via `komorebic`'s configuration commands.
|
||||
|
||||
If the `komorebi` process has been started with the `--ffm` flag, you can
|
||||
enable focus follows mouse behaviour in the `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"focus_follows_mouse": "Komorebi"
|
||||
}
|
||||
```
|
||||
|
||||
When calling any of the `komorebic` commands related to focus-follows-mouse
|
||||
functionality, the `windows` implementation will be chosen as the default
|
||||
implementation. You can optionally specify the `komorebi` implementation by
|
||||
passing it as an argument to the `--implementation` flag:
|
||||
|
||||
```powershell
|
||||
komorebic.exe toggle-focus-follows-mouse --implementation komorebi
|
||||
```
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ applications are [already generated for
|
||||
you](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
|
||||
Sometimes you will want a specific application to never be tiled, and instead
|
||||
float all the time. You can add rules to enforce this behaviour in the
|
||||
be completely ignored. You can add rules to enforce this behaviour in the
|
||||
`komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"float_rules": [
|
||||
"ignore_rules": [
|
||||
{
|
||||
"kind": "Title",
|
||||
"id": "Media Player",
|
||||
|
||||
19
docs/common-workflows/multiple-bar-instances.md
Normal file
19
docs/common-workflows/multiple-bar-instances.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Multiple Bar Instances
|
||||
|
||||
If you would like to run multiple instances of `komorebi-bar` to target different monitors, it is possible to do so
|
||||
by maintaining multiple `komorebi.bar.json` configuration files and specifying their paths in the `bar_configurations`
|
||||
array in your `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"bar_configurations": [
|
||||
"C:/Users/LGUG2Z/komorebi.bar.monitor1.json",
|
||||
"C:/Users/LGUG2Z/komorebi.bar.monitor2.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You may also use `$Env:USERPROFILE` or `$Env:KOMOREBI_CONFIG_HOME` when specifying the paths.
|
||||
|
||||
The main difference between different `komorebi.bar.json` files will be the value of `monitor.index` which is used to
|
||||
target the monitor for each instance of `komorebi-bar`.
|
||||
@@ -62,7 +62,7 @@ using `default_workspace_padding` and `default_container_padding`.
|
||||
|
||||
You may have seen videos and screenshots of people using `komorebi` with a
|
||||
thick, colourful active window border. You can also enable this by setting
|
||||
`active_window_border` to `true`. However, please be warned that this feature
|
||||
`border` to `true`. However, please be warned that this feature
|
||||
is a crude hack trying to compensate for the insistence of Microsoft Windows
|
||||
design teams to make custom borders with widths that are actually visible to
|
||||
the user a thing of the past and removing this capability from the Win32 API.
|
||||
@@ -162,6 +162,8 @@ If you have an ultrawide monitor, I recommend using this layout.
|
||||
|
||||
If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layouts) this is almost exactly the same!
|
||||
|
||||
The `grid` layout does not support resizing windows tiles.
|
||||
|
||||
```
|
||||
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||
| | | | | | | | | | | | | | |
|
||||
@@ -177,7 +179,7 @@ If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layou
|
||||
|
||||
`whkd` is a fairly basic piece of software with a simple configuration format:
|
||||
key bindings go to the left of the colon, and shell commands go to the right of the
|
||||
colon. By default, the `whkdrc` file should be located in the `$Env:USERPROFILE/.config/` directory.
|
||||
colon.
|
||||
|
||||
Please remember that `whkd` does not support overriding Microsoft's limitations
|
||||
on hotkey bindings that include the `Windows` key. If this is important to you,
|
||||
@@ -188,6 +190,25 @@ bindings for `komorebic` commands instead.
|
||||
{% include "./whkdrc.sample" %}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
`whkd` searches for a `whkdrc` configuration file in the following locations:
|
||||
|
||||
* `$Env:WHKD_CONFIG_HOME`
|
||||
* `$Env:USERPROFILE/.config`
|
||||
|
||||
It is also possible to change a hotkey behavior depending on which application has focus:
|
||||
|
||||
```
|
||||
alt + n [
|
||||
# ProcessName as shown by `Get-Process`
|
||||
Firefox : echo "hello firefox"
|
||||
|
||||
# Spaces are fine, no quotes required
|
||||
Google Chrome : echo "hello chrome"
|
||||
]
|
||||
```
|
||||
|
||||
### Setting .shell
|
||||
|
||||
There is one special directive at the top of the file, `.shell` which can be
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
`komorebi` is a tiling window manager that works as an extension to Microsoft's
|
||||
[Desktop Window
|
||||
Manager](https://docs.microsoft.com/en-us/windows/win32/dwm/dwm-overview) in
|
||||
@@ -15,12 +17,63 @@ system and desktop environment by default. Users are free to make such
|
||||
modifications in their own configuration files for `komorebi`, but these will
|
||||
always remain opt-in and off-by-default.
|
||||
|
||||
## Community
|
||||
|
||||
There is a [Discord server](https://discord.gg/mGkn66PHkx) available for
|
||||
`komorebi`-related discussion, help, troubleshooting etc. If you have any
|
||||
specific feature requests or bugs to report, please create an issue on
|
||||
[GitHub](https://github.com/LGUG2Z/komorebi).
|
||||
`komorebi`-related discussion, help, troubleshooting etc.
|
||||
|
||||
There is a [YouTube
|
||||
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
|
||||
`komorebi` development videos, feature previews and release overviews. Subscribing
|
||||
to the channel (which is monetized as part of the YouTube Partner Program) and
|
||||
watching videos is a really simple and passive way to contribute financially to
|
||||
the development and maintenance of `komorebi`.
|
||||
|
||||
There is also a [YouTube
|
||||
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg?sub_confirmation=1)
|
||||
where I share `komorebi` live programming videos and tutorial videos.
|
||||
There is an [Awesome List](https://github.com/LGUG2Z/awesome-komorebi) which
|
||||
showcases the many awesome projects that exist in the `komorebi` ecosystem.
|
||||
|
||||
## Licensing for Personal Use
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 1.0.0 license](https://github.com/LGUG2Z/komorebi-license), 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` for personal use other than redistribution, or distribution of
|
||||
new works (i.e. hard-forks) based on the software.
|
||||
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended either for personal use or for integration
|
||||
back upstream via pull requests.
|
||||
|
||||
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
|
||||
i.e. using `komorebi` at work).
|
||||
|
||||
## Sponsorship for Personal Use
|
||||
|
||||
`komorebi` is a free and educational source project, and one that encourages you
|
||||
to make charitable donations if you find the software to be useful and have the
|
||||
financial means.
|
||||
|
||||
I encourage you to make a charitable donation to the [Palestine Children's
|
||||
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or to contribute
|
||||
to a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring
|
||||
me on GitHub.
|
||||
|
||||
[GitHub Sponsors is enabled for this
|
||||
project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on
|
||||
the Discord server, get shout-outs at the end of _komorebi_-related videos on
|
||||
YouTube, and gain the ability to submit feature requests on the issue tracker.
|
||||
|
||||
If you would like to tip or sponsor the project but are unable to use GitHub
|
||||
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
|
||||
make an anonymous Bitcoin donation to `bc1qv73wzspc77k46uty4vp85x8sdp24mphvm58f6q`.
|
||||
|
||||
## Licensing for Commercial Use
|
||||
|
||||
A dedicated Individual Commercial Use License is available for those who want to
|
||||
use `komorebi` at work.
|
||||
|
||||
The Individual Commerical Use License adds “Commercial Use” as a “Permitted Use”
|
||||
for the licensed individual only, for the duration of a valid paid license
|
||||
subscription only. All provisions and restrictions enumerated in the [Komorebi
|
||||
License](https://github.com/LGUG2Z/komorebi-license) continue to apply.
|
||||
|
||||
More information, pricing and purchase links for Individual Commercial Use
|
||||
Licenses [can be found here](https://lgug2z.com/software/komorebi).
|
||||
|
||||
@@ -134,6 +134,26 @@ an offline machine to install.
|
||||
Once installed, proceed to get the [example configurations](example-configurations.md) (none of the commands for
|
||||
first-time set up and running komorebi require an internet connection).
|
||||
|
||||
## Upgrades
|
||||
|
||||
Before upgrading, make sure to run `komorebic stop --whkd --bar`. This is to ensure that all the current
|
||||
komorebi-related exe files can be replaced without issue.
|
||||
|
||||
Then, depending on whether you installed via `scoop` or `winget`, you can run the appropriate command:
|
||||
|
||||
```powershell
|
||||
# for winget
|
||||
winget upgrade LGUG2Z.komorebi
|
||||
```
|
||||
|
||||
```powershell
|
||||
# for scoop
|
||||
scoop update komorebi
|
||||
```
|
||||
|
||||
Once the upgrade is completed you can confirm that you have the latest version by running `komorebic --version`, and
|
||||
then start it with `komorebic start --whkd --bar`.
|
||||
|
||||
## Uninstallation
|
||||
|
||||
Before uninstalling, first run `komorebic stop --whkd --bar` to make sure that
|
||||
@@ -147,7 +167,7 @@ files created by the `quickstart` command and any other runtime files:
|
||||
|
||||
```powershell
|
||||
rm $Env:USERPROFILE\komorebi.json
|
||||
rm $Env:USERPROFILE\applications.yaml
|
||||
rm $Env:USERPROFILE\applications.json
|
||||
rm $Env:USERPROFILE\.config\whkdrc
|
||||
rm -r -Force $Env:LOCALAPPDATA\komorebi
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.bar.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.31/schema.bar.json",
|
||||
"monitor": {
|
||||
"index": 0,
|
||||
"work_area_offset": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.31/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
"default_workspace_padding": 20,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# v0.1.22
|
||||
|
||||
In addition to the [changelog](https://github.com/LGUG2Z/komorebi/releases/tag/v0.1.22) of new features and fixes,
|
||||
please note the following changes from `v0.1.21` to adjust your configuration files accordingly.
|
||||
|
||||
## tl;dr
|
||||
|
||||
The way windows are sized and drawn has been improved to remove the need to manually specify and remove invisible
|
||||
borders for applications that overflow them. If you use the active window border, the first time you launch `v0.1.22`
|
||||
you may end up with a _huge_ border due to these changes.
|
||||
|
||||
`active_window_border_width` and `active_window_border_offset` have been renamed to `border_width` and `border_offset`
|
||||
as they now also apply outside the context of the active window border.
|
||||
|
||||
```json
|
||||
{
|
||||
"active_window_border": true,
|
||||
"border_width": 8,
|
||||
"border_offset": -1
|
||||
}
|
||||
```
|
||||
|
||||
Users of the active window border should start from these settings and read the notes below before making further
|
||||
adjustments.
|
||||
|
||||
## Changes to `active_window_border`, and window sizing:
|
||||
|
||||
- The border no longer creates a second drop-shadow around the active window
|
||||
- Windows are now sized to fill the layout region entirely, ignoring window decorations such as drop shadows
|
||||
- Border offset now starts exactly at the paint edge of the window on all sides
|
||||
- Windows are sized such that the border offset and border width are taken into account
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
### Gapless
|
||||
|
||||
- Disable "transparency effects" Personalization > Colors
|
||||
- Set the following settings in `komorebi.json`:
|
||||
```json
|
||||
{
|
||||
"default_workspace_padding": 0,
|
||||
"default_container_padding": 0,
|
||||
"border_offset": -1,
|
||||
"border_width": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 1px border
|
||||
|
||||
A 1px border is drawn around the window edge. Users may see a gap for a single pixel, if the system theme has a
|
||||
transparent edge - this is the windows themed edge, and is not present for all applications.
|
||||
|
||||
```json
|
||||
{
|
||||
"border_offset": 0,
|
||||
"border_width": 1
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,15 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Phantom Tiles
|
||||
|
||||
Sometimes you may experience an application which leaves "ghost tiles" on a workspace, where there is space reserved for
|
||||
a window but no window visible.
|
||||
|
||||
You can ignore these windows by following these steps:
|
||||
|
||||
* Run `komorebic visible-windows` to find details about the invisible window
|
||||
* Using that information, [create a rule to ignore that window](common-workflows/ignore-windows.md)
|
||||
|
||||
## AutoHotKey executable not found
|
||||
|
||||
If you try to start komorebi with AHK using `komorebic start --ahk`, and you have
|
||||
@@ -85,10 +95,10 @@ running `komorebic stop` and `komorebic start`.
|
||||
|
||||
To avoid waiting an eternity:
|
||||
|
||||
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
|
||||
Settings_
|
||||
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
|
||||
Settings_
|
||||
|
||||
_Turn off the display: 1 minute_
|
||||
_Turn off the display: 1 minute_
|
||||
|
||||
Allow a minute for the display to reset, then once it actually shuts off
|
||||
allow for any additional time as prompted by your monitor for the cycle to
|
||||
|
||||
31
justfile
31
justfile
@@ -1,4 +1,5 @@
|
||||
set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"]
|
||||
|
||||
export RUST_BACKTRACE := "full"
|
||||
|
||||
clean:
|
||||
@@ -26,17 +27,17 @@ install:
|
||||
run target:
|
||||
cargo +stable run --bin {{ target }} --locked
|
||||
|
||||
warn $RUST_LOG="warn":
|
||||
just run
|
||||
warn target $RUST_LOG="warn":
|
||||
just run {{ target }}
|
||||
|
||||
info $RUST_LOG="info":
|
||||
just run
|
||||
info target $RUST_LOG="info":
|
||||
just run {{ target }}
|
||||
|
||||
debug $RUST_LOG="debug":
|
||||
just run
|
||||
debug target $RUST_LOG="debug":
|
||||
just run {{ target }}
|
||||
|
||||
trace $RUST_LOG="trace":
|
||||
just run
|
||||
trace target $RUST_LOG="trace":
|
||||
just run {{ target }}
|
||||
|
||||
deadlock $RUST_LOG="trace":
|
||||
cargo +stable run --bin komorebi --locked --features deadlock_detection
|
||||
@@ -45,13 +46,15 @@ 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:
|
||||
jsonschema:
|
||||
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
|
||||
# this part is run in a nix shell because python is a nightmare
|
||||
schemagen:
|
||||
rm -rf static-config-docs bar-config-docs
|
||||
mkdir -p static-config-docs bar-config-docs
|
||||
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/
|
||||
mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.30"
|
||||
version = "0.1.32"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -16,7 +16,7 @@ crossbeam-channel = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui-phosphor = "0.7"
|
||||
egui-phosphor = "0.8"
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
@@ -31,7 +31,6 @@ 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" }
|
||||
@@ -5,6 +5,10 @@ use crate::config::PositionConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiNotificationState;
|
||||
use crate::process_hwnd;
|
||||
use crate::render::Color32Ext;
|
||||
use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::render::RenderExt;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widget::WidgetConfig;
|
||||
use crate::BAR_HEIGHT;
|
||||
@@ -14,6 +18,8 @@ use crate::MONITOR_RIGHT;
|
||||
use crate::MONITOR_TOP;
|
||||
use crossbeam_channel::Receiver;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Align2;
|
||||
use eframe::egui::Area;
|
||||
use eframe::egui::CentralPanel;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
@@ -22,13 +28,17 @@ use eframe::egui::FontDefinitions;
|
||||
use eframe::egui::FontFamily;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Id;
|
||||
use eframe::egui::Layout;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Rgba;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Visuals;
|
||||
use font_loader::system_fonts;
|
||||
use font_loader::system_fonts::FontPropertyBuilder;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use komorebi_themes::Base16Value;
|
||||
use komorebi_themes::Catppuccin;
|
||||
@@ -41,16 +51,28 @@ use std::sync::Arc;
|
||||
|
||||
pub struct Komobar {
|
||||
pub config: Arc<KomobarConfig>,
|
||||
pub render_config: Rc<RefCell<RenderConfig>>,
|
||||
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub center_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 bg_color_with_alpha: Rc<RefCell<Color32>>,
|
||||
pub scale_factor: f32,
|
||||
applied_theme_on_first_frame: bool,
|
||||
}
|
||||
|
||||
pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Color32>>) {
|
||||
pub fn apply_theme(
|
||||
ctx: &Context,
|
||||
theme: KomobarTheme,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
bg_color_with_alpha: Rc<RefCell<Color32>>,
|
||||
transparency_alpha: Option<u8>,
|
||||
grouping: Option<Grouping>,
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
match theme {
|
||||
KomobarTheme::Catppuccin {
|
||||
name: catppuccin,
|
||||
@@ -130,6 +152,29 @@ pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Colo
|
||||
bg_color.replace(base16.background());
|
||||
}
|
||||
}
|
||||
|
||||
// Apply transparency_alpha
|
||||
let theme_color = *bg_color.borrow();
|
||||
|
||||
bg_color_with_alpha.replace(theme_color.try_apply_alpha(transparency_alpha));
|
||||
|
||||
// apply rounding to the widgets
|
||||
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) =
|
||||
&grouping
|
||||
{
|
||||
if let Some(rounding) = config.rounding {
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.rounding = rounding.into();
|
||||
style.visuals.widgets.inactive.rounding = rounding.into();
|
||||
style.visuals.widgets.hovered.rounding = rounding.into();
|
||||
style.visuals.widgets.active.rounding = rounding.into();
|
||||
style.visuals.widgets.open.rounding = rounding.into();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update RenderConfig's background_color so that widgets will have the new color
|
||||
render_config.borrow_mut().background_color = *bg_color.borrow();
|
||||
}
|
||||
|
||||
impl Komobar {
|
||||
@@ -171,6 +216,10 @@ impl Komobar {
|
||||
y: BAR_HEIGHT,
|
||||
});
|
||||
|
||||
if end.y == 0.0 {
|
||||
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
|
||||
}
|
||||
|
||||
let rect = komorebi_client::Rect {
|
||||
left: start.x as i32,
|
||||
top: start.y as i32,
|
||||
@@ -189,9 +238,141 @@ impl Komobar {
|
||||
}
|
||||
}
|
||||
|
||||
self.try_apply_theme(config, ctx);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
self.render_config.replace(config.new_renderconfig(
|
||||
ctx,
|
||||
*self.bg_color.borrow(),
|
||||
config.icon_scale,
|
||||
));
|
||||
|
||||
let mut komorebi_notification_state = previous_notification_state;
|
||||
let mut komorebi_widgets = Vec::new();
|
||||
|
||||
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
|
||||
if let WidgetConfig::Komorebi(config) = widget_config {
|
||||
komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Left));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(center_widgets) = &config.center_widgets {
|
||||
for (idx, widget_config) in center_widgets.iter().enumerate() {
|
||||
if let WidgetConfig::Komorebi(config) = widget_config {
|
||||
komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Center));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
|
||||
if let WidgetConfig::Komorebi(config) = widget_config {
|
||||
komorebi_widgets.push((Komorebi::from(config), idx, Alignment::Right));
|
||||
}
|
||||
}
|
||||
|
||||
let mut left_widgets = config
|
||||
.left_widgets
|
||||
.iter()
|
||||
.filter(|config| config.enabled())
|
||||
.map(|config| config.as_boxed_bar_widget())
|
||||
.collect::<Vec<Box<dyn BarWidget>>>();
|
||||
|
||||
let mut center_widgets = match &config.center_widgets {
|
||||
Some(center_widgets) => center_widgets
|
||||
.iter()
|
||||
.filter(|config| config.enabled())
|
||||
.map(|config| config.as_boxed_bar_widget())
|
||||
.collect::<Vec<Box<dyn BarWidget>>>(),
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let mut right_widgets = config
|
||||
.right_widgets
|
||||
.iter()
|
||||
.filter(|config| config.enabled())
|
||||
.map(|config| config.as_boxed_bar_widget())
|
||||
.collect::<Vec<Box<dyn BarWidget>>>();
|
||||
|
||||
if !komorebi_widgets.is_empty() {
|
||||
komorebi_widgets
|
||||
.into_iter()
|
||||
.for_each(|(mut widget, idx, side)| {
|
||||
match komorebi_notification_state {
|
||||
None => {
|
||||
komorebi_notification_state =
|
||||
Some(widget.komorebi_notification_state.clone());
|
||||
}
|
||||
Some(ref previous) => {
|
||||
if widget.workspaces.map_or(false, |w| w.enable) {
|
||||
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 {
|
||||
Alignment::Left => left_widgets[idx] = boxed,
|
||||
Alignment::Center => center_widgets[idx] = boxed,
|
||||
Alignment::Right => right_widgets[idx] = boxed,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
right_widgets.reverse();
|
||||
|
||||
self.left_widgets = left_widgets;
|
||||
self.center_widgets = center_widgets;
|
||||
self.right_widgets = right_widgets;
|
||||
|
||||
if let (Some(prev_rect), Some(new_rect)) = (
|
||||
&self.config.monitor.work_area_offset,
|
||||
&config.monitor.work_area_offset,
|
||||
) {
|
||||
if new_rect != prev_rect {
|
||||
if let Err(error) = komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(config.monitor.index, *new_rect),
|
||||
) {
|
||||
tracing::error!(
|
||||
"error applying work area offset to monitor '{}': {}",
|
||||
config.monitor.index,
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
"work area offset applied to monitor: {}",
|
||||
config.monitor.index
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("widget configuration options applied");
|
||||
|
||||
self.komorebi_notification_state = komorebi_notification_state;
|
||||
|
||||
self.config = config.clone().into();
|
||||
}
|
||||
|
||||
fn try_apply_theme(&mut self, config: &KomobarConfig, ctx: &Context) {
|
||||
match config.theme {
|
||||
Some(theme) => {
|
||||
apply_theme(ctx, theme, self.bg_color.clone());
|
||||
apply_theme(
|
||||
ctx,
|
||||
theme,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
config.transparency_alpha,
|
||||
config.grouping,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|
||||
@@ -207,11 +388,21 @@ impl Komobar {
|
||||
},
|
||||
);
|
||||
|
||||
let bar_transparency_alpha = config.transparency_alpha;
|
||||
let bar_grouping = config.grouping;
|
||||
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());
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
bar_transparency_alpha,
|
||||
bar_grouping,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
|
||||
let stack_accent = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
@@ -232,81 +423,30 @@ impl Komobar {
|
||||
Err(_) => {
|
||||
ctx.set_style(Style::default());
|
||||
self.bg_color.replace(Style::default().visuals.panel_fill);
|
||||
|
||||
// apply rounding to the widgets since we didn't call `apply_theme`
|
||||
if let Some(
|
||||
Grouping::Bar(config)
|
||||
| Grouping::Alignment(config)
|
||||
| Grouping::Widget(config),
|
||||
) = &bar_grouping
|
||||
{
|
||||
if let Some(rounding) = config.rounding {
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.rounding = rounding.into();
|
||||
style.visuals.widgets.inactive.rounding = rounding.into();
|
||||
style.visuals.widgets.hovered.rounding = rounding.into();
|
||||
style.visuals.widgets.active.rounding = rounding.into();
|
||||
style.visuals.widgets.open.rounding = rounding.into();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
@@ -315,13 +455,17 @@ impl Komobar {
|
||||
) -> Self {
|
||||
let mut komobar = Self {
|
||||
config: config.clone(),
|
||||
render_config: Rc::new(RefCell::new(RenderConfig::new())),
|
||||
komorebi_notification_state: None,
|
||||
left_widgets: vec![],
|
||||
center_widgets: vec![],
|
||||
right_widgets: vec![],
|
||||
rx_gui,
|
||||
rx_config,
|
||||
bg_color: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
|
||||
bg_color_with_alpha: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
|
||||
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
|
||||
applied_theme_on_first_frame: false,
|
||||
};
|
||||
|
||||
komobar.apply_config(&cc.egui_ctx, &config, None);
|
||||
@@ -365,7 +509,7 @@ impl Komobar {
|
||||
if let Some((font, _)) = system_fonts::get(&property) {
|
||||
fonts
|
||||
.font_data
|
||||
.insert(name.to_owned(), FontData::from_owned(font));
|
||||
.insert(name.to_owned(), Arc::new(FontData::from_owned(font)));
|
||||
|
||||
fonts
|
||||
.families
|
||||
@@ -385,13 +529,10 @@ impl Komobar {
|
||||
}
|
||||
}
|
||||
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
|
||||
// }
|
||||
// Needed for transparency
|
||||
fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
|
||||
Rgba::TRANSPARENT.to_array()
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
|
||||
@@ -419,40 +560,117 @@ impl eframe::App for Komobar {
|
||||
self.config.monitor.index,
|
||||
self.rx_gui.clone(),
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if !self.applied_theme_on_first_frame {
|
||||
self.try_apply_theme(&self.config.clone(), ctx);
|
||||
self.applied_theme_on_first_frame = true;
|
||||
}
|
||||
|
||||
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())
|
||||
.fill(*self.bg_color_with_alpha.borrow())
|
||||
} else {
|
||||
Frame::none().fill(*self.bg_color.borrow())
|
||||
Frame::none().fill(*self.bg_color_with_alpha.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);
|
||||
}
|
||||
});
|
||||
let mut render_config = self.render_config.borrow_mut();
|
||||
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
for w in &mut self.right_widgets {
|
||||
w.render(ctx, ui);
|
||||
}
|
||||
})
|
||||
})
|
||||
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
||||
|
||||
CentralPanel::default().frame(frame).show(ctx, |_| {
|
||||
// Apply grouping logic for the bar as a whole
|
||||
let area_frame = if let Some(frame) = &self.config.frame {
|
||||
Frame::none().inner_margin(Margin::symmetric(0.0, frame.inner_margin.y))
|
||||
} else {
|
||||
Frame::none()
|
||||
};
|
||||
|
||||
if !self.left_widgets.is_empty() {
|
||||
// Left-aligned widgets layout
|
||||
Area::new(Id::new("left_panel"))
|
||||
.anchor(Align2::LEFT_CENTER, [0.0, 0.0]) // Align in the left center of the window
|
||||
.show(ctx, |ui| {
|
||||
let mut left_area_frame = area_frame;
|
||||
if let Some(frame) = &self.config.frame {
|
||||
left_area_frame.inner_margin.left = frame.inner_margin.x;
|
||||
}
|
||||
left_area_frame.show(ui, |ui| {
|
||||
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
|
||||
let mut render_conf = render_config.clone();
|
||||
render_conf.alignment = Some(Alignment::Left);
|
||||
|
||||
render_config.apply_on_alignment(ui, |ui| {
|
||||
for w in &mut self.left_widgets {
|
||||
w.render(ctx, ui, &mut render_conf);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if !self.right_widgets.is_empty() {
|
||||
// Right-aligned widgets layout
|
||||
Area::new(Id::new("right_panel"))
|
||||
.anchor(Align2::RIGHT_CENTER, [0.0, 0.0]) // Align in the right center of the window
|
||||
.show(ctx, |ui| {
|
||||
let mut right_area_frame = area_frame;
|
||||
if let Some(frame) = &self.config.frame {
|
||||
right_area_frame.inner_margin.right = frame.inner_margin.x;
|
||||
}
|
||||
right_area_frame.show(ui, |ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
let mut render_conf = render_config.clone();
|
||||
render_conf.alignment = Some(Alignment::Right);
|
||||
|
||||
render_config.apply_on_alignment(ui, |ui| {
|
||||
for w in &mut self.right_widgets {
|
||||
w.render(ctx, ui, &mut render_conf);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if !self.center_widgets.is_empty() {
|
||||
// Floating center widgets
|
||||
Area::new(Id::new("center_panel"))
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0]) // Align in the center of the window
|
||||
.show(ctx, |ui| {
|
||||
let center_area_frame = area_frame;
|
||||
center_area_frame.show(ui, |ui| {
|
||||
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
|
||||
let mut render_conf = render_config.clone();
|
||||
render_conf.alignment = Some(Alignment::Center);
|
||||
|
||||
render_config.apply_on_alignment(ui, |ui| {
|
||||
for w in &mut self.center_widgets {
|
||||
w.render(ctx, ui, &mut render_conf);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Side {
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
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;
|
||||
@@ -30,37 +29,18 @@ pub struct BatteryConfig {
|
||||
|
||||
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}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
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(),
|
||||
manager: Manager::new().unwrap(),
|
||||
last_state: String::new(),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
state: BatteryState::Discharging,
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +95,7 @@ impl Battery {
|
||||
}
|
||||
|
||||
impl BarWidget for Battery {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
@@ -124,19 +104,12 @@ impl BarWidget for Battery {
|
||||
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(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -144,17 +117,22 @@ impl BarWidget for Battery {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
ui.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::render::Grouping;
|
||||
use crate::widget::WidgetConfig;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
@@ -11,12 +12,12 @@ 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`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.32`
|
||||
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)
|
||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
|
||||
pub frame: Option<FrameConfig>,
|
||||
/// Monitor options
|
||||
pub monitor: MonitorConfig,
|
||||
@@ -24,12 +25,22 @@ pub struct KomobarConfig {
|
||||
pub font_family: Option<String>,
|
||||
/// Font size (default: 12.5)
|
||||
pub font_size: Option<f32>,
|
||||
/// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
|
||||
pub icon_scale: Option<f32>,
|
||||
/// Max label width before text truncation (default: 400.0)
|
||||
pub max_label_width: Option<f32>,
|
||||
/// Theme
|
||||
pub theme: Option<KomobarTheme>,
|
||||
/// Alpha value for the color transparency [[0-255]] (default: 200)
|
||||
pub transparency_alpha: Option<u8>,
|
||||
/// Spacing between widgets (default: 10.0)
|
||||
pub widget_spacing: Option<f32>,
|
||||
/// Visual grouping for widgets
|
||||
pub grouping: Option<Grouping>,
|
||||
/// Left side widgets (ordered left-to-right)
|
||||
pub left_widgets: Vec<WidgetConfig>,
|
||||
/// Center widgets (ordered left-to-right)
|
||||
pub center_widgets: Option<Vec<WidgetConfig>>,
|
||||
/// Right side widgets (ordered left-to-right)
|
||||
pub right_widgets: Vec<WidgetConfig>,
|
||||
}
|
||||
@@ -136,11 +147,13 @@ impl From<Position> for Pos2 {
|
||||
pub enum KomobarTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
|
||||
name: komorebi_themes::Base16,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
@@ -176,3 +189,17 @@ pub enum LabelPrefix {
|
||||
/// Show an icon and text
|
||||
IconAndText,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum DisplayFormat {
|
||||
/// Show only icon
|
||||
Icon,
|
||||
/// Show only text
|
||||
Text,
|
||||
/// Show an icon and text for the selected element, and text on the rest
|
||||
TextAndIconOnSelected,
|
||||
/// Show both icon and text
|
||||
IconAndText,
|
||||
/// Show an icon and text for the selected element, and icons on the rest
|
||||
IconAndTextOnSelected,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
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;
|
||||
@@ -30,17 +29,18 @@ pub struct CpuConfig {
|
||||
|
||||
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();
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
system: System::new_with_specifics(
|
||||
RefreshKind::default().without_memory().without_processes(),
|
||||
),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,17 +70,10 @@ impl Cpu {
|
||||
}
|
||||
|
||||
impl BarWidget for Cpu {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 => {
|
||||
@@ -88,7 +81,7 @@ impl BarWidget for Cpu {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -96,25 +89,27 @@ impl BarWidget for Cpu {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
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()
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
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;
|
||||
@@ -86,17 +85,10 @@ impl Date {
|
||||
}
|
||||
|
||||
impl BarWidget for Date {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 => {
|
||||
@@ -104,7 +96,7 @@ impl BarWidget for Date {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -116,22 +108,28 @@ impl BarWidget for Date {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.next()
|
||||
}
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false),
|
||||
)
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.format.next()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
use crate::bar::apply_theme;
|
||||
use crate::config::DisplayFormat;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::komorebi_layout::KomorebiLayout;
|
||||
use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::ICON_CACHE;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::WIDGET_SPACING;
|
||||
use crate::MONITOR_INDEX;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::vec2;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ColorImage;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Image;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::SelectableLabel;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Rounding;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Stroke;
|
||||
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::Container;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::Window;
|
||||
use komorebi_client::Workspace;
|
||||
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;
|
||||
@@ -39,7 +46,7 @@ use std::sync::atomic::Ordering;
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiConfig {
|
||||
/// Configure the Workspaces widget
|
||||
pub workspaces: KomorebiWorkspacesConfig,
|
||||
pub workspaces: Option<KomorebiWorkspacesConfig>,
|
||||
/// Configure the Layout widget
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
/// Configure the Focused Window widget
|
||||
@@ -54,20 +61,28 @@ pub struct KomorebiWorkspacesConfig {
|
||||
pub enable: bool,
|
||||
/// Hide workspaces without any windows
|
||||
pub hide_empty_workspaces: bool,
|
||||
/// Display format of the workspace
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiLayoutConfig {
|
||||
/// Enable the Komorebi Layout widget
|
||||
pub enable: bool,
|
||||
/// List of layout options
|
||||
pub options: Option<Vec<KomorebiLayout>>,
|
||||
/// Display format of the current layout
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused window)
|
||||
pub show_icon: Option<bool>,
|
||||
/// Display format of the currently focused window
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -98,14 +113,18 @@ impl From<&KomorebiConfig> for Komorebi {
|
||||
selected_workspace: String::new(),
|
||||
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
workspaces: vec![],
|
||||
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
|
||||
hide_empty_workspaces: value
|
||||
.workspaces
|
||||
.map(|w| w.hide_empty_workspaces)
|
||||
.unwrap_or_default(),
|
||||
mouse_follows_focus: true,
|
||||
work_area_offset: None,
|
||||
focused_container_information: (vec![], vec![], 0),
|
||||
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
|
||||
stack_accent: None,
|
||||
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
|
||||
})),
|
||||
workspaces: value.workspaces,
|
||||
layout: value.layout,
|
||||
layout: value.layout.clone(),
|
||||
focused_window: value.focused_window,
|
||||
configuration_switcher,
|
||||
}
|
||||
@@ -115,107 +134,166 @@ impl From<&KomorebiConfig> for Komorebi {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Komorebi {
|
||||
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
|
||||
pub workspaces: KomorebiWorkspacesConfig,
|
||||
pub workspaces: Option<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) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
|
||||
let icon_size = Vec2::splat(config.icon_font_id.size);
|
||||
|
||||
if self.workspaces.enable {
|
||||
let mut update = None;
|
||||
if let Some(workspaces) = self.workspaces {
|
||||
if 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_notification_state.workspaces.is_empty() {
|
||||
let format = workspaces.display.unwrap_or(DisplayFormat::Text);
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
|
||||
proceed = false;
|
||||
}
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for (i, (ws, container_information)) in
|
||||
komorebi_notification_state.workspaces.iter().enumerate()
|
||||
{
|
||||
if SelectableFrame::new(
|
||||
komorebi_notification_state.selected_workspace.eq(ws),
|
||||
)
|
||||
.show(ui, |ui| {
|
||||
let mut has_icon = false;
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: FocusWorkspaceNumber");
|
||||
proceed = false;
|
||||
}
|
||||
if format == DisplayFormat::Icon
|
||||
|| format == DisplayFormat::IconAndText
|
||||
|| format == DisplayFormat::IconAndTextOnSelected
|
||||
|| (format == DisplayFormat::TextAndIconOnSelected
|
||||
&& komorebi_notification_state.selected_workspace.eq(ws))
|
||||
{
|
||||
let icons: Vec<_> =
|
||||
container_information.icons.iter().flatten().collect();
|
||||
|
||||
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 !icons.is_empty() {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::same(
|
||||
ui.style().spacing.button_padding.y,
|
||||
))
|
||||
.show(ui, |ui| {
|
||||
for icon in icons {
|
||||
ui.add(
|
||||
Image::from(&img_to_texture(ctx, icon))
|
||||
.maintain_aspect_ratio(true)
|
||||
.fit_to_exact_size(icon_size),
|
||||
);
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::RetileWithResizeDimensions)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: Retile");
|
||||
}
|
||||
if !has_icon {
|
||||
has_icon = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// draw a custom icon when there is no app icon
|
||||
if match format {
|
||||
DisplayFormat::Icon => !has_icon,
|
||||
_ => false,
|
||||
} {
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(icon_size, Sense::hover());
|
||||
let stroke = Stroke::new(
|
||||
1.0,
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
);
|
||||
let mut rect = response.rect;
|
||||
let rounding = Rounding::same(rect.width() * 0.1);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke);
|
||||
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
|
||||
|
||||
response.on_hover_text(ws.to_string())
|
||||
} else if match format {
|
||||
DisplayFormat::Icon => has_icon,
|
||||
_ => false,
|
||||
} {
|
||||
ui.response().on_hover_text(ws.to_string())
|
||||
} else if format != DisplayFormat::IconAndTextOnSelected
|
||||
|| (format == DisplayFormat::IconAndTextOnSelected
|
||||
&& komorebi_notification_state.selected_workspace.eq(ws))
|
||||
{
|
||||
ui.add(Label::new(ws.to_string()).selectable(false))
|
||||
} else {
|
||||
ui.response()
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
update = Some(ws.to_string());
|
||||
|
||||
if komorebi_notification_state.mouse_follows_focus {
|
||||
if komorebi_client::send_batch([
|
||||
SocketMessage::MouseFollowsFocus(false),
|
||||
SocketMessage::FocusMonitorWorkspaceNumber(
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
),
|
||||
SocketMessage::RetileWithResizeDimensions,
|
||||
SocketMessage::MouseFollowsFocus(true),
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send the following batch of messages to komorebi:\n
|
||||
MouseFollowsFocus(false)\n
|
||||
FocusMonitorWorkspaceNumber({}, {})\n
|
||||
RetileWithResizeDimensions
|
||||
MouseFollowsFocus(true)\n",
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
);
|
||||
}
|
||||
} else if komorebi_client::send_batch([
|
||||
SocketMessage::FocusMonitorWorkspaceNumber(
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
),
|
||||
SocketMessage::RetileWithResizeDimensions,
|
||||
])
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send the following batch of messages to komorebi:\n
|
||||
FocusMonitorWorkspaceNumber({}, {})\n
|
||||
RetileWithResizeDimensions",
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(update) = update {
|
||||
komorebi_notification_state.selected_workspace = update;
|
||||
}
|
||||
}
|
||||
|
||||
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 => {}
|
||||
}
|
||||
}
|
||||
if let Some(layout_config) = &self.layout {
|
||||
if layout_config.enable {
|
||||
let workspace_idx: Option<usize> = komorebi_notification_state
|
||||
.workspaces
|
||||
.iter()
|
||||
.position(|o| komorebi_notification_state.selected_workspace.eq(&o.0));
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
komorebi_notification_state.layout.show(
|
||||
ctx,
|
||||
ui,
|
||||
config,
|
||||
layout_config,
|
||||
workspace_idx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,170 +301,167 @@ impl BarWidget for Komorebi {
|
||||
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()))
|
||||
if path.is_file() {
|
||||
config.apply_on_widget(false, ui,|ui|{
|
||||
if SelectableFrame::new(false).show(ui, |ui|{
|
||||
ui.add(Label::new(name).selectable(false))
|
||||
})
|
||||
.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;
|
||||
}
|
||||
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!(
|
||||
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"
|
||||
);
|
||||
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 titles = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.titles;
|
||||
if !titles.is_empty() {
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
let icons = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.icons;
|
||||
let focused_window_idx = komorebi_notification_state
|
||||
.focused_container_information
|
||||
.focused_window_idx;
|
||||
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
let len = iter.len();
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (i, (title, icon)) in iter.enumerate() {
|
||||
let selected = i == focused_window_idx && len != 1;
|
||||
|
||||
if i == focused_window_idx {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
if SelectableFrame::new(selected)
|
||||
.show(ui, |ui| {
|
||||
// handle legacy setting
|
||||
let format = focused_window.display.unwrap_or(
|
||||
if focused_window.show_icon.unwrap_or(false) {
|
||||
DisplayFormat::IconAndText
|
||||
} else {
|
||||
DisplayFormat::Text
|
||||
},
|
||||
);
|
||||
|
||||
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 format == DisplayFormat::Icon
|
||||
|| format == DisplayFormat::IconAndText
|
||||
|| format == DisplayFormat::IconAndTextOnSelected
|
||||
|| (format == DisplayFormat::TextAndIconOnSelected
|
||||
&& i == focused_window_idx)
|
||||
{
|
||||
if let Some(img) = icon {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::same(
|
||||
ui.style().spacing.button_padding.y,
|
||||
))
|
||||
.show(ui, |ui| {
|
||||
let response = ui.add(
|
||||
Image::from(&img_to_texture(ctx, img))
|
||||
.maintain_aspect_ratio(true)
|
||||
.fit_to_exact_size(icon_size),
|
||||
);
|
||||
|
||||
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 let DisplayFormat::Icon = format {
|
||||
response.on_hover_text(title);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
if format == DisplayFormat::Text
|
||||
|| format == DisplayFormat::IconAndText
|
||||
|| format == DisplayFormat::TextAndIconOnSelected
|
||||
|| (format == DisplayFormat::IconAndTextOnSelected
|
||||
&& i == focused_window_idx)
|
||||
{
|
||||
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(),
|
||||
);
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
if selected {
|
||||
return;
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
if komorebi_notification_state.mouse_follows_focus {
|
||||
if komorebi_client::send_batch([
|
||||
SocketMessage::MouseFollowsFocus(false),
|
||||
SocketMessage::FocusStackWindow(i),
|
||||
SocketMessage::MouseFollowsFocus(true),
|
||||
]).is_err() {
|
||||
tracing::error!(
|
||||
"could not send the following batch of messages to komorebi:\n
|
||||
MouseFollowsFocus(false)\n
|
||||
FocusStackWindow({})\n
|
||||
MouseFollowsFocus(true)\n",
|
||||
i,
|
||||
);
|
||||
}
|
||||
} else if komorebi_client::send_message(
|
||||
&SocketMessage::FocusStackWindow(i)
|
||||
).is_err() {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusStackWindow"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,33 +475,15 @@ fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationState {
|
||||
pub workspaces: Vec<String>,
|
||||
pub workspaces: Vec<(String, KomorebiNotificationStateContainerInformation)>,
|
||||
pub selected_workspace: String,
|
||||
pub focused_container_information: (Vec<String>, Vec<Option<RgbaImage>>, usize),
|
||||
pub focused_container_information: KomorebiNotificationStateContainerInformation,
|
||||
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"),
|
||||
}
|
||||
}
|
||||
pub monitor_index: usize,
|
||||
}
|
||||
|
||||
impl KomorebiNotificationState {
|
||||
@@ -434,12 +491,18 @@ impl KomorebiNotificationState {
|
||||
self.hide_empty_workspaces = config.hide_empty_workspaces;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn handle_notification(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
monitor_index: usize,
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
bg_color_with_alpha: Rc<RefCell<Color32>>,
|
||||
transparency_alpha: Option<u8>,
|
||||
grouping: Option<Grouping>,
|
||||
default_theme: Option<KomobarTheme>,
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
match rx_gui.try_recv() {
|
||||
Err(error) => match error {
|
||||
@@ -451,17 +514,56 @@ impl KomorebiNotificationState {
|
||||
}
|
||||
},
|
||||
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");
|
||||
match notification.event {
|
||||
NotificationEvent::WindowManager(_) => {}
|
||||
NotificationEvent::Socket(message) => match message {
|
||||
SocketMessage::ReloadStaticConfiguration(path) => {
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
} else if let Some(default_theme) = default_theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
default_theme,
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("removed theme from updated komorebi.json and applied default theme");
|
||||
} else {
|
||||
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color,
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
|
||||
self.monitor_index = monitor_index;
|
||||
|
||||
self.mouse_follows_focus = notification.state.mouse_follows_focus;
|
||||
|
||||
let monitor = ¬ification.state.monitors.elements()[monitor_index];
|
||||
@@ -477,70 +579,148 @@ impl KomorebiNotificationState {
|
||||
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
|
||||
|
||||
for (i, ws) in monitor.workspaces().iter().enumerate() {
|
||||
let should_add = if self.hide_empty_workspaces {
|
||||
let should_show = 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)));
|
||||
if should_show {
|
||||
workspaces.push((
|
||||
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
|
||||
ws.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
if monitor.workspaces()[focused_workspace_idx]
|
||||
.monocle_container()
|
||||
.is_some()
|
||||
{
|
||||
self.layout = KomorebiLayout::Monocle;
|
||||
} else if !*monitor.workspaces()[focused_workspace_idx].tile() {
|
||||
self.layout = KomorebiLayout::Floating;
|
||||
}
|
||||
|
||||
if notification.state.is_paused {
|
||||
} else if notification.state.is_paused {
|
||||
self.layout = KomorebiLayout::Paused;
|
||||
} else {
|
||||
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
|
||||
komorebi_client::Layout::Default(layout) => {
|
||||
KomorebiLayout::Default(*layout)
|
||||
}
|
||||
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
self.focused_container_information =
|
||||
(&monitor.workspaces()[focused_workspace_idx]).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationStateContainerInformation {
|
||||
pub titles: Vec<String>,
|
||||
pub icons: Vec<Option<RgbaImage>>,
|
||||
pub focused_window_idx: usize,
|
||||
}
|
||||
|
||||
impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Workspace) -> Self {
|
||||
let mut container_info = Self::EMPTY;
|
||||
|
||||
if let Some(container) = value.monocle_container() {
|
||||
container_info = container.into();
|
||||
} else if let Some(container) = value.focused_container() {
|
||||
container_info = container.into();
|
||||
}
|
||||
|
||||
for floating_window in value.floating_windows() {
|
||||
if floating_window.is_focused() {
|
||||
container_info = floating_window.into();
|
||||
}
|
||||
}
|
||||
|
||||
container_info
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Container) -> Self {
|
||||
let windows = value.windows().iter().collect::<Vec<_>>();
|
||||
let mut icons = vec![];
|
||||
|
||||
for window in windows {
|
||||
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
||||
let mut update_cache = false;
|
||||
let exe = window.exe().unwrap_or_default();
|
||||
|
||||
match icon_cache.get(&exe) {
|
||||
None => {
|
||||
icons.push(windows_icons::get_icon_by_process_id(window.process_id()));
|
||||
update_cache = true;
|
||||
}
|
||||
Some(icon) => {
|
||||
icons.push(Some(icon.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if update_cache {
|
||||
if let Some(Some(icon)) = icons.last() {
|
||||
icon_cache.insert(exe, icon.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
titles: value
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.title().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
icons,
|
||||
focused_window_idx: value.focused_window_idx(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Window> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Window) -> Self {
|
||||
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
||||
let mut update_cache = false;
|
||||
let mut icons = vec![];
|
||||
let exe = value.exe().unwrap_or_default();
|
||||
|
||||
match icon_cache.get(&exe) {
|
||||
None => {
|
||||
icons.push(windows_icons::get_icon_by_process_id(value.process_id()));
|
||||
update_cache = true;
|
||||
}
|
||||
Some(icon) => {
|
||||
icons.push(Some(icon.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if update_cache {
|
||||
if let Some(Some(icon)) = icons.last() {
|
||||
icon_cache.insert(exe, icon.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
titles: vec![value.title().unwrap_or_default()],
|
||||
icons,
|
||||
focused_window_idx: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KomorebiNotificationStateContainerInformation {
|
||||
pub const EMPTY: Self = Self {
|
||||
titles: vec![],
|
||||
icons: vec![],
|
||||
focused_window_idx: 0,
|
||||
};
|
||||
}
|
||||
|
||||
304
komorebi-bar/src/komorebi_layout.rs
Normal file
304
komorebi-bar/src/komorebi_layout.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
use crate::config::DisplayFormat;
|
||||
use crate::komorebi::KomorebiLayoutConfig;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use eframe::egui::vec2;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Rounding;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::SocketMessage;
|
||||
use schemars::JsonSchema;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde_json::from_str;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum KomorebiLayout {
|
||||
Default(komorebi_client::DefaultLayout),
|
||||
Monocle,
|
||||
Floating,
|
||||
Paused,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: String = String::deserialize(deserializer)?;
|
||||
|
||||
// Attempt to deserialize the string as a DefaultLayout
|
||||
if let Ok(default_layout) =
|
||||
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
|
||||
{
|
||||
return Ok(KomorebiLayout::Default(default_layout));
|
||||
}
|
||||
|
||||
// Handle other cases
|
||||
match s.as_str() {
|
||||
"Monocle" => Ok(KomorebiLayout::Monocle),
|
||||
"Floating" => Ok(KomorebiLayout::Floating),
|
||||
"Paused" => Ok(KomorebiLayout::Paused),
|
||||
"Custom" => Ok(KomorebiLayout::Custom),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for KomorebiLayout {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
|
||||
KomorebiLayout::Monocle => write!(f, "Monocle"),
|
||||
KomorebiLayout::Floating => write!(f, "Floating"),
|
||||
KomorebiLayout::Paused => write!(f, "Paused"),
|
||||
KomorebiLayout::Custom => write!(f, "Custom"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KomorebiLayout {
|
||||
fn is_default(&mut self) -> bool {
|
||||
matches!(self, KomorebiLayout::Default(_))
|
||||
}
|
||||
|
||||
fn on_click(
|
||||
&mut self,
|
||||
show_options: &bool,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: Option<usize>,
|
||||
) -> bool {
|
||||
if self.is_default() {
|
||||
!show_options
|
||||
} else {
|
||||
self.on_click_option(monitor_idx, workspace_idx);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
|
||||
match self {
|
||||
KomorebiLayout::Default(option) => {
|
||||
if let Some(ws_idx) = workspace_idx {
|
||||
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
||||
monitor_idx,
|
||||
ws_idx,
|
||||
*option,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
||||
}
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Monocle => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() {
|
||||
tracing::error!("could not send message to komorebi: ToggleMonocle");
|
||||
}
|
||||
}
|
||||
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 => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_icon(&mut self, font_id: FontId, ctx: &Context, ui: &mut Ui) {
|
||||
// paint custom icons for the layout
|
||||
let size = Vec2::splat(font_id.size);
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::hover());
|
||||
let color = ctx.style().visuals.selection.stroke.color;
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
let mut rect = response.rect;
|
||||
let rounding = Rounding::same(rect.width() * 0.1);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke);
|
||||
|
||||
match self {
|
||||
KomorebiLayout::Default(layout) => match layout {
|
||||
komorebi_client::DefaultLayout::BSP => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::Columns => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::Rows => {
|
||||
painter.line_segment([c - vec2(r, r / 2.0), c + vec2(r, -r / 2.0)], stroke);
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(r, -r / 2.0), c + vec2(r, r / 2.0)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::VerticalStack => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(r, 0.0), c], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::HorizontalStack => {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c, c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::Grid => {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
},
|
||||
KomorebiLayout::Monocle => {}
|
||||
KomorebiLayout::Floating => {
|
||||
let mut rect_left = response.rect;
|
||||
rect_left.set_width(rect.width() * 0.5);
|
||||
rect_left.set_height(rect.height() * 0.5);
|
||||
let mut rect_right = rect_left;
|
||||
rect_left = rect_left.translate(Vec2::new(
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
));
|
||||
rect_right = rect_right.translate(Vec2::new(
|
||||
rect.width() * 0.35 + stroke.width,
|
||||
rect.width() * 0.35 + stroke.width,
|
||||
));
|
||||
painter.rect_filled(rect_left, rounding, color);
|
||||
painter.rect_stroke(rect_right, rounding, stroke);
|
||||
}
|
||||
KomorebiLayout::Paused => {
|
||||
let mut rect_left = response.rect;
|
||||
rect_left.set_width(rect.width() * 0.25);
|
||||
rect_left.set_height(rect.height() * 0.8);
|
||||
let mut rect_right = rect_left;
|
||||
rect_left = rect_left.translate(Vec2::new(
|
||||
rect.width() * 0.2 + stroke.width,
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
));
|
||||
rect_right = rect_right.translate(Vec2::new(
|
||||
rect.width() * 0.55 + stroke.width,
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
));
|
||||
painter.rect_filled(rect_left, rounding, color);
|
||||
painter.rect_filled(rect_right, rounding, color);
|
||||
}
|
||||
KomorebiLayout::Custom => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c + vec2(0.0, r / 2.0), c + vec2(r, r / 2.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r / 3.0), c - vec2(r, r / 3.0)], stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
ui: &mut Ui,
|
||||
render_config: &mut RenderConfig,
|
||||
layout_config: &KomorebiLayoutConfig,
|
||||
workspace_idx: Option<usize>,
|
||||
) {
|
||||
let monitor_idx = render_config.monitor_idx;
|
||||
let font_id = render_config.icon_font_id.clone();
|
||||
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
|
||||
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);
|
||||
|
||||
if !self.is_default() {
|
||||
show_options = false;
|
||||
}
|
||||
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
let layout_frame = SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
|
||||
self.show_icon(font_id.clone(), ctx, ui);
|
||||
}
|
||||
|
||||
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
|
||||
ui.add(Label::new(self.to_string()).selectable(false));
|
||||
}
|
||||
})
|
||||
.on_hover_text(self.to_string());
|
||||
|
||||
if layout_frame.clicked() {
|
||||
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
|
||||
}
|
||||
|
||||
if show_options {
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
Frame::none().show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||
.selectable(false),
|
||||
);
|
||||
|
||||
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::HorizontalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
||||
//KomorebiLayout::Custom,
|
||||
KomorebiLayout::Monocle,
|
||||
KomorebiLayout::Floating,
|
||||
KomorebiLayout::Paused,
|
||||
]);
|
||||
|
||||
for layout_option in &mut layout_options {
|
||||
if SelectableFrame::new(self == layout_option)
|
||||
.show(ui, |ui| layout_option.show_icon(font_id.clone(), ctx, ui))
|
||||
.on_hover_text(match layout_option {
|
||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
||||
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
||||
KomorebiLayout::Custom => "Custom".to_string(),
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
||||
show_options = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
RenderConfig::store_show_komorebi_layout_options(show_options);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,12 @@ mod config;
|
||||
mod cpu;
|
||||
mod date;
|
||||
mod komorebi;
|
||||
mod komorebi_layout;
|
||||
mod media;
|
||||
mod memory;
|
||||
mod network;
|
||||
mod render;
|
||||
mod selected_frame;
|
||||
mod storage;
|
||||
mod time;
|
||||
mod ui;
|
||||
@@ -21,15 +24,20 @@ use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::SubscribeOptions;
|
||||
use schemars::gen::SchemaSettings;
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
@@ -42,14 +50,16 @@ 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 MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
|
||||
pub static BAR_HEIGHT: f32 = 50.0;
|
||||
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
struct Opts {
|
||||
@@ -234,6 +244,8 @@ fn main() -> color_eyre::Result<()> {
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_INDEX.store(config.monitor.index, Ordering::SeqCst);
|
||||
|
||||
match config.position {
|
||||
None => {
|
||||
config.position = Some(PositionConfig {
|
||||
@@ -266,7 +278,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let viewport_builder = ViewportBuilder::default()
|
||||
.with_decorations(false)
|
||||
// .with_transparent(config.transparent)
|
||||
.with_transparent(true)
|
||||
.with_taskbar(false);
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
@@ -339,6 +351,10 @@ fn main() -> color_eyre::Result<()> {
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(subscription) => {
|
||||
match subscription.set_read_timeout(Some(Duration::from_secs(1))) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(subscription);
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
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::Align;
|
||||
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;
|
||||
@@ -78,20 +77,13 @@ impl Media {
|
||||
}
|
||||
|
||||
impl BarWidget for Media {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -99,29 +91,33 @@ impl BarWidget for Media {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
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);
|
||||
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(),
|
||||
)
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
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;
|
||||
@@ -30,17 +29,18 @@ pub struct MemoryConfig {
|
||||
|
||||
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();
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
system: System::new_with_specifics(
|
||||
RefreshKind::default().without_cpu().without_processes(),
|
||||
),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,17 +73,10 @@ impl Memory {
|
||||
}
|
||||
|
||||
impl BarWidget for Memory {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 => {
|
||||
@@ -91,7 +84,7 @@ impl BarWidget for Memory {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -99,25 +92,27 @@ impl BarWidget for Memory {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
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()
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
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;
|
||||
@@ -27,6 +26,8 @@ pub struct NetworkConfig {
|
||||
pub show_total_data_transmitted: bool,
|
||||
/// Show network activity
|
||||
pub show_network_activity: bool,
|
||||
/// Show default interface
|
||||
pub show_default_interface: Option<bool>,
|
||||
/// Characters to reserve for network activity data
|
||||
pub network_activity_fill_characters: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
@@ -37,130 +38,40 @@ pub struct NetworkConfig {
|
||||
|
||||
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(),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
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,
|
||||
show_total_activity: value.show_total_data_transmitted,
|
||||
show_activity: value.show_network_activity,
|
||||
show_default_interface: value.show_default_interface.unwrap_or(true),
|
||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
||||
default_interface: String::new(),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
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(),
|
||||
last_state_total_activity: vec![],
|
||||
last_state_activity: vec![],
|
||||
last_updated_network_activity: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Network {
|
||||
pub enable: bool,
|
||||
pub show_total_data_transmitted: bool,
|
||||
pub show_network_activity: bool,
|
||||
networks_total_data_transmitted: Networks,
|
||||
pub show_total_activity: bool,
|
||||
pub show_activity: bool,
|
||||
pub show_default_interface: bool,
|
||||
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_state_total_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
last_updated_network_activity: Instant,
|
||||
network_activity_fill_characters: usize,
|
||||
}
|
||||
@@ -174,214 +85,273 @@ impl Network {
|
||||
}
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> Vec<String> {
|
||||
let mut outputs = self.last_state_network_activity.clone();
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let mut activity = self.last_state_activity.clone();
|
||||
let mut total_activity = self.last_state_total_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)
|
||||
if now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
outputs.clear();
|
||||
activity.clear();
|
||||
total_activity.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,
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
|
||||
self.networks_network_activity.refresh();
|
||||
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if self.show_activity {
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
Self::to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval,
|
||||
),
|
||||
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,
|
||||
Self::to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval,
|
||||
),
|
||||
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,
|
||||
)
|
||||
}
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
if self.show_total_activity {
|
||||
total_activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Total,
|
||||
Self::to_pretty_bytes(data.total_received(), 1),
|
||||
Self::to_pretty_bytes(data.total_transmitted(), 1),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_network_activity.clone_from(&outputs);
|
||||
self.last_state_activity.clone_from(&activity);
|
||||
self.last_state_total_activity.clone_from(&total_activity);
|
||||
self.last_updated_network_activity = now;
|
||||
}
|
||||
|
||||
outputs
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
fn total_data_transmitted(&mut self) -> Vec<String> {
|
||||
let mut outputs = self.last_state_total_data_transmitted.clone();
|
||||
let now = Instant::now();
|
||||
fn reading_to_label(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
reading: NetworkReading,
|
||||
config: RenderConfig,
|
||||
) -> Label {
|
||||
let (text_down, text_up) = match self.label_prefix {
|
||||
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"{: >width$}/s | ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"{: >width$}/s",
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("{} | ", reading.received_text),
|
||||
reading.transmitted_text,
|
||||
),
|
||||
},
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"DOWN: {: >width$}/s | ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"UP: {: >width$}/s",
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("\u{2211}DOWN: {}/s | ", reading.received_text),
|
||||
format!("\u{2211}UP: {}/s", reading.transmitted_text),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
if self.show_total_data_transmitted
|
||||
&& now.duration_since(self.last_updated_total_data_transmitted)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
outputs.clear();
|
||||
let icon_format = TextFormat::simple(
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
);
|
||||
let text_format = TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
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),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// icon
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
|
||||
}
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
icon_format.font_id.clone(),
|
||||
icon_format.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
self.last_state_total_data_transmitted.clone_from(&outputs);
|
||||
self.last_updated_total_data_transmitted = now;
|
||||
// text
|
||||
layout_job.append(
|
||||
&text_down,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
// icon
|
||||
layout_job.append(
|
||||
&match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ARROW_FAT_UP.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
0.0,
|
||||
icon_format.clone(),
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job.append(
|
||||
&text_up,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
Label::new(layout_job).selectable(false)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
outputs
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
self.default_interface();
|
||||
// widget spacing: make sure to use the same config to call the apply_on_widget function
|
||||
let mut render_config = config.clone();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
if self.show_total_activity || self.show_activity {
|
||||
let (activity, total_activity) = self.network_activity();
|
||||
|
||||
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: ");
|
||||
if self.show_total_activity {
|
||||
for reading in total_activity {
|
||||
render_config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading, config.clone()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if self.show_activity {
|
||||
for reading in activity {
|
||||
render_config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading, config.clone()));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
if self.show_default_interface {
|
||||
self.default_interface();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
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(),
|
||||
},
|
||||
config.icon_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 {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// widget spacing: pass on the config that was use for calling the apply_on_widget function
|
||||
*config = render_config.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum NetworkReadingFormat {
|
||||
Speed = 0,
|
||||
Total = 1,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NetworkReading {
|
||||
pub format: NetworkReadingFormat,
|
||||
pub received_text: String,
|
||||
pub transmitted_text: String,
|
||||
}
|
||||
|
||||
impl NetworkReading {
|
||||
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
|
||||
NetworkReading {
|
||||
format,
|
||||
received_text: received,
|
||||
transmitted_text: transmitted,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,22 +374,3 @@ impl fmt::Display for DataUnit {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
390
komorebi-bar/src/render.rs
Normal file
390
komorebi-bar/src/render.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::KomobarConfig;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::InnerResponse;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Rounding;
|
||||
use eframe::egui::Shadow;
|
||||
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::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum Grouping {
|
||||
/// No grouping is applied
|
||||
None,
|
||||
/// Widgets are grouped as a whole
|
||||
Bar(GroupingConfig),
|
||||
/// Widgets are grouped by alignment
|
||||
Alignment(GroupingConfig),
|
||||
/// Widgets are grouped individually
|
||||
Widget(GroupingConfig),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RenderConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub monitor_idx: usize,
|
||||
/// Spacing between widgets
|
||||
pub spacing: f32,
|
||||
/// Sets how widgets are grouped
|
||||
pub grouping: Grouping,
|
||||
/// Background color
|
||||
pub background_color: Color32,
|
||||
/// Alignment of the widgets
|
||||
pub alignment: Option<Alignment>,
|
||||
/// Add more inner margin when adding a widget group
|
||||
pub more_inner_margin: bool,
|
||||
/// Set to true after the first time the apply_on_widget was called on an alignment
|
||||
pub applied_on_widget: bool,
|
||||
/// FontId for text
|
||||
pub text_font_id: FontId,
|
||||
/// FontId for icon (based on scaling the text font id)
|
||||
pub icon_font_id: FontId,
|
||||
}
|
||||
|
||||
pub trait RenderExt {
|
||||
fn new_renderconfig(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
background_color: Color32,
|
||||
icon_scale: Option<f32>,
|
||||
) -> RenderConfig;
|
||||
}
|
||||
|
||||
impl RenderExt for &KomobarConfig {
|
||||
fn new_renderconfig(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
background_color: Color32,
|
||||
icon_scale: Option<f32>,
|
||||
) -> RenderConfig {
|
||||
let text_font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut icon_font_id = text_font_id.clone();
|
||||
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
|
||||
|
||||
RenderConfig {
|
||||
monitor_idx: self.monitor.index,
|
||||
spacing: self.widget_spacing.unwrap_or(10.0),
|
||||
grouping: self.grouping.unwrap_or(Grouping::None),
|
||||
background_color,
|
||||
alignment: None,
|
||||
more_inner_margin: false,
|
||||
applied_on_widget: false,
|
||||
text_font_id,
|
||||
icon_font_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderConfig {
|
||||
pub fn load_show_komorebi_layout_options() -> bool {
|
||||
SHOW_KOMOREBI_LAYOUT_OPTIONS.load(Ordering::SeqCst) != 0
|
||||
}
|
||||
|
||||
pub fn store_show_komorebi_layout_options(show: bool) {
|
||||
SHOW_KOMOREBI_LAYOUT_OPTIONS.store(show as usize, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
monitor_idx: 0,
|
||||
spacing: 0.0,
|
||||
grouping: Grouping::None,
|
||||
background_color: Color32::BLACK,
|
||||
alignment: None,
|
||||
more_inner_margin: false,
|
||||
applied_on_widget: false,
|
||||
text_font_id: FontId::default(),
|
||||
icon_font_id: FontId::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_frame_on_bar(
|
||||
&mut self,
|
||||
frame: Frame,
|
||||
ui_style: &Arc<eframe::egui::Style>,
|
||||
) -> Frame {
|
||||
self.alignment = None;
|
||||
|
||||
if let Grouping::Bar(config) = self.grouping {
|
||||
return self.define_group_frame(
|
||||
//TODO: this outer margin can be a config
|
||||
Some(Margin {
|
||||
left: 10.0,
|
||||
right: 10.0,
|
||||
top: 6.0,
|
||||
bottom: 6.0,
|
||||
}),
|
||||
config,
|
||||
ui_style,
|
||||
);
|
||||
}
|
||||
|
||||
frame
|
||||
}
|
||||
|
||||
pub fn apply_on_alignment<R>(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.alignment = None;
|
||||
|
||||
if let Grouping::Alignment(config) = self.grouping {
|
||||
return self.define_group(None, config, ui, add_contents);
|
||||
}
|
||||
|
||||
Self::fallback_group(ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn apply_on_widget<R>(
|
||||
&mut self,
|
||||
more_inner_margin: bool,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.more_inner_margin = more_inner_margin;
|
||||
let outer_margin = self.widget_outer_margin(ui);
|
||||
|
||||
if let Grouping::Widget(config) = self.grouping {
|
||||
return self.define_group(Some(outer_margin), config, ui, add_contents);
|
||||
}
|
||||
|
||||
self.fallback_widget_group(Some(outer_margin), ui, add_contents)
|
||||
}
|
||||
|
||||
fn fallback_group<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||
InnerResponse {
|
||||
inner: add_contents(ui),
|
||||
response: ui.response().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_widget_group<R>(
|
||||
&mut self,
|
||||
outer_margin: Option<Margin>,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
Frame::none()
|
||||
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
|
||||
.inner_margin(match self.more_inner_margin {
|
||||
true => Margin::symmetric(5.0, 0.0),
|
||||
false => Margin::same(0.0),
|
||||
})
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
|
||||
fn define_group<R>(
|
||||
&mut self,
|
||||
outer_margin: Option<Margin>,
|
||||
config: GroupingConfig,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.define_group_frame(outer_margin, config, ui.style())
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn define_group_frame(
|
||||
&mut self,
|
||||
outer_margin: Option<Margin>,
|
||||
config: GroupingConfig,
|
||||
ui_style: &Arc<eframe::egui::Style>,
|
||||
) -> Frame {
|
||||
Frame::group(ui_style)
|
||||
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
|
||||
.inner_margin(match self.more_inner_margin {
|
||||
true => Margin::symmetric(6.0, 1.0),
|
||||
false => Margin::symmetric(1.0, 1.0),
|
||||
})
|
||||
.stroke(ui_style.visuals.widgets.noninteractive.bg_stroke)
|
||||
.rounding(match config.rounding {
|
||||
Some(rounding) => rounding.into(),
|
||||
None => ui_style.visuals.widgets.noninteractive.rounding,
|
||||
})
|
||||
.fill(
|
||||
self.background_color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
)
|
||||
.shadow(match config.style {
|
||||
Some(style) => match style {
|
||||
// new styles can be added if needed here
|
||||
GroupingStyle::Default => Shadow::NONE,
|
||||
GroupingStyle::DefaultWithShadowB4O1S3 => Shadow {
|
||||
blur: 4.0,
|
||||
offset: Vec2::new(1.0, 1.0),
|
||||
spread: 3.0,
|
||||
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithShadowB4O0S3 => Shadow {
|
||||
blur: 4.0,
|
||||
offset: Vec2::new(0.0, 0.0),
|
||||
spread: 3.0,
|
||||
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithShadowB0O1S3 => Shadow {
|
||||
blur: 0.0,
|
||||
offset: Vec2::new(1.0, 1.0),
|
||||
spread: 3.0,
|
||||
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithGlowB3O1S2 => Shadow {
|
||||
blur: 3.0,
|
||||
offset: Vec2::new(1.0, 1.0),
|
||||
spread: 2.0,
|
||||
color: ui_style
|
||||
.visuals
|
||||
.selection
|
||||
.stroke
|
||||
.color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithGlowB3O0S2 => Shadow {
|
||||
blur: 3.0,
|
||||
offset: Vec2::new(0.0, 0.0),
|
||||
spread: 2.0,
|
||||
color: ui_style
|
||||
.visuals
|
||||
.selection
|
||||
.stroke
|
||||
.color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithGlowB0O1S2 => Shadow {
|
||||
blur: 0.0,
|
||||
offset: Vec2::new(1.0, 1.0),
|
||||
spread: 2.0,
|
||||
color: ui_style
|
||||
.visuals
|
||||
.selection
|
||||
.stroke
|
||||
.color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
},
|
||||
None => Shadow::NONE,
|
||||
})
|
||||
}
|
||||
|
||||
fn widget_outer_margin(&mut self, ui: &mut Ui) -> Margin {
|
||||
let spacing = if self.applied_on_widget {
|
||||
// Remove the default item spacing from the margin
|
||||
self.spacing - ui.spacing().item_spacing.x
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if !self.applied_on_widget {
|
||||
self.applied_on_widget = true;
|
||||
}
|
||||
|
||||
Margin {
|
||||
left: match self.alignment {
|
||||
Some(align) => match align {
|
||||
Alignment::Left => spacing,
|
||||
Alignment::Center => spacing,
|
||||
Alignment::Right => 0.0,
|
||||
},
|
||||
None => 0.0,
|
||||
},
|
||||
right: match self.alignment {
|
||||
Some(align) => match align {
|
||||
Alignment::Left => 0.0,
|
||||
Alignment::Center => 0.0,
|
||||
Alignment::Right => spacing,
|
||||
},
|
||||
None => 0.0,
|
||||
},
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GroupingConfig {
|
||||
/// Styles for the grouping
|
||||
pub style: Option<GroupingStyle>,
|
||||
/// Alpha value for the color transparency [[0-255]] (default: 200)
|
||||
pub transparency_alpha: Option<u8>,
|
||||
/// Rounding values for the 4 corners. Can be a single or 4 values.
|
||||
pub rounding: Option<RoundingConfig>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum GroupingStyle {
|
||||
#[serde(alias = "CtByte")]
|
||||
Default,
|
||||
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
|
||||
#[serde(alias = "CtByteWithShadow")]
|
||||
#[serde(alias = "DefaultWithShadow")]
|
||||
DefaultWithShadowB4O1S3,
|
||||
/// A shadow is added under the default group. (blur: 4, offset: x-0 y-0, spread: 3)
|
||||
DefaultWithShadowB4O0S3,
|
||||
/// A shadow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 3)
|
||||
DefaultWithShadowB0O1S3,
|
||||
/// A glow is added under the default group. (blur: 3, offset: x-1 y-1, spread: 2)
|
||||
DefaultWithGlowB3O1S2,
|
||||
/// A glow is added under the default group. (blur: 3, offset: x-0 y-0, spread: 2)
|
||||
DefaultWithGlowB3O0S2,
|
||||
/// A glow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 2)
|
||||
DefaultWithGlowB0O1S2,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum RoundingConfig {
|
||||
/// All 4 corners are the same
|
||||
Same(f32),
|
||||
/// All 4 corners are custom. Order: NW, NE, SW, SE
|
||||
Individual([f32; 4]),
|
||||
}
|
||||
|
||||
impl From<RoundingConfig> for Rounding {
|
||||
fn from(value: RoundingConfig) -> Self {
|
||||
match value {
|
||||
RoundingConfig::Same(value) => Rounding::same(value),
|
||||
RoundingConfig::Individual(values) => Self {
|
||||
nw: values[0],
|
||||
ne: values[1],
|
||||
sw: values[2],
|
||||
se: values[3],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Color32Ext {
|
||||
fn try_apply_alpha(self, transparency_alpha: Option<u8>) -> Self;
|
||||
}
|
||||
|
||||
impl Color32Ext for Color32 {
|
||||
/// Tries to apply the alpha value to the Color32
|
||||
fn try_apply_alpha(self, transparency_alpha: Option<u8>) -> Self {
|
||||
if let Some(alpha) = transparency_alpha {
|
||||
return Color32::from_rgba_unmultiplied(self.r(), self.g(), self.b(), alpha);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
56
komorebi-bar/src/selected_frame.rs
Normal file
56
komorebi-bar/src/selected_frame.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use eframe::egui::CursorIcon;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Response;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Ui;
|
||||
|
||||
/// Same as SelectableLabel, but supports all content
|
||||
pub struct SelectableFrame {
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl SelectableFrame {
|
||||
pub fn new(selected: bool) -> Self {
|
||||
Self { selected }
|
||||
}
|
||||
|
||||
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
|
||||
let Self { selected } = self;
|
||||
|
||||
Frame::none()
|
||||
.show(ui, |ui| {
|
||||
let response = ui.interact(ui.max_rect(), ui.unique_id(), Sense::click());
|
||||
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
let inner_margin = Margin::symmetric(
|
||||
ui.style().spacing.button_padding.x,
|
||||
ui.style().spacing.button_padding.y,
|
||||
);
|
||||
|
||||
if selected
|
||||
|| response.hovered()
|
||||
|| response.highlighted()
|
||||
|| response.has_focus()
|
||||
{
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
Frame::none()
|
||||
.stroke(visuals.bg_stroke)
|
||||
.rounding(visuals.rounding)
|
||||
.fill(visuals.bg_fill)
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
} else {
|
||||
Frame::none()
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
})
|
||||
.inner
|
||||
.on_hover_cursor(CursorIcon::PointingHand)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
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;
|
||||
@@ -79,15 +78,8 @@ impl Storage {
|
||||
}
|
||||
|
||||
impl BarWidget for Storage {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 {
|
||||
@@ -96,7 +88,7 @@ impl BarWidget for Storage {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -104,30 +96,31 @@ impl BarWidget for Storage {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
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()
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
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;
|
||||
@@ -77,17 +76,10 @@ impl Time {
|
||||
}
|
||||
|
||||
impl BarWidget for Time {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 => {
|
||||
@@ -95,7 +87,7 @@ impl BarWidget for Time {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -107,22 +99,23 @@ impl BarWidget for Time {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.toggle()
|
||||
}
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
self.format.toggle()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::memory::Memory;
|
||||
use crate::memory::MemoryConfig;
|
||||
use crate::network::Network;
|
||||
use crate::network::NetworkConfig;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::storage::Storage;
|
||||
use crate::storage::StorageConfig;
|
||||
use crate::time::Time;
|
||||
@@ -23,7 +24,7 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub trait BarWidget {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui);
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -53,4 +54,26 @@ impl WidgetConfig {
|
||||
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
match self {
|
||||
WidgetConfig::Battery(config) => config.enable,
|
||||
WidgetConfig::Cpu(config) => config.enable,
|
||||
WidgetConfig::Date(config) => config.enable,
|
||||
WidgetConfig::Komorebi(config) => {
|
||||
config.workspaces.as_ref().map_or(false, |w| w.enable)
|
||||
|| config.layout.as_ref().map_or(false, |w| w.enable)
|
||||
|| config.focused_window.as_ref().map_or(false, |w| w.enable)
|
||||
|| config
|
||||
.configuration_switcher
|
||||
.as_ref()
|
||||
.map_or(false, |w| w.enable)
|
||||
}
|
||||
WidgetConfig::Media(config) => config.enable,
|
||||
WidgetConfig::Memory(config) => config.enable,
|
||||
WidgetConfig::Network(config) => config.enable,
|
||||
WidgetConfig::Storage(config) => config.enable,
|
||||
WidgetConfig::Time(config) => config.enable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.30"
|
||||
version = "0.1.32"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::animation::prefix::AnimationPrefix;
|
||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||
pub use komorebi::colour::Colour;
|
||||
pub use komorebi::colour::Rgb;
|
||||
@@ -54,6 +55,7 @@ use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::Shutdown;
|
||||
use std::time::Duration;
|
||||
pub use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
@@ -62,13 +64,30 @@ const KOMOREBI: &str = "komorebi.sock";
|
||||
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
|
||||
if let Ok(m_str) = serde_json::to_string(&m) {
|
||||
s.push_str(&m_str);
|
||||
s.push('\n');
|
||||
}
|
||||
s
|
||||
});
|
||||
stream.write_all(msgs.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_read_timeout(Some(Duration::from_secs(1)))?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.30"
|
||||
version = "0.1.32"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.30"
|
||||
version = "0.1.32"
|
||||
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"] }
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "24362c4" }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f85cc3c", default-features = false, features = ["egui30"] }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_variant = "0.1"
|
||||
strum = "0.26"
|
||||
@@ -4,10 +4,12 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub use base16_egui_themes::Base16;
|
||||
pub use catppuccin_egui;
|
||||
pub use eframe::egui::Color32;
|
||||
use serde_variant::to_variant_name;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -24,6 +26,28 @@ pub enum Theme {
|
||||
},
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn variant_names(&self) -> Vec<String> {
|
||||
match self {
|
||||
Theme::Catppuccin { .. } => {
|
||||
vec![
|
||||
"Frappe".to_string(),
|
||||
"Latte".to_string(),
|
||||
"Macchiato".to_string(),
|
||||
"Mocha".to_string(),
|
||||
]
|
||||
}
|
||||
Theme::Base16 { .. } => Base16::iter()
|
||||
.map(|variant| {
|
||||
to_variant_name(&variant)
|
||||
.expect("could not convert to variant name")
|
||||
.to_string()
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Base16Value {
|
||||
Base00,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.30"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
version = "0.1.32"
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -44,7 +42,6 @@ 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 }
|
||||
|
||||
115
komorebi/src/animation/animation_manager.rs
Normal file
115
komorebi/src/animation/animation_manager.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::prefix::AnimationPrefix;
|
||||
|
||||
#[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<String, 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, animation_key: &str) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(animation_key) {
|
||||
animation_state.pending_cancel_count > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_progress(&self, animation_key: &str) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(animation_key) {
|
||||
animation_state.in_progress
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_cancel(&mut self, animation_key: &str) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
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, animation_key: &str) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_cancel(&mut self, animation_key: &str) {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.pending_cancel_count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, animation_key: &str) {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.in_progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, animation_key: &str) {
|
||||
if let Entry::Vacant(e) = self.animations.entry(animation_key.to_string()) {
|
||||
e.insert(AnimationState {
|
||||
in_progress: true,
|
||||
cancel_idx_counter: 0,
|
||||
pending_cancel_count: 0,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.in_progress = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&mut self, animation_key: &str) {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.in_progress = false;
|
||||
|
||||
if animation_state.pending_cancel_count == 0 {
|
||||
self.animations.remove(animation_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count_in_progress(&self, animation_key_prefix: AnimationPrefix) -> usize {
|
||||
self.animations
|
||||
.keys()
|
||||
.filter(|key| key.starts_with(animation_key_prefix.to_string().as_str()))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.animations.len()
|
||||
}
|
||||
}
|
||||
122
komorebi/src/animation/engine.rs
Normal file
122
komorebi/src/animation/engine.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::RenderDispatcher;
|
||||
use super::ANIMATION_DURATION_GLOBAL;
|
||||
use super::ANIMATION_FPS;
|
||||
use super::ANIMATION_MANAGER;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct AnimationEngine;
|
||||
|
||||
impl AnimationEngine {
|
||||
pub fn wait_for_all_animations() {
|
||||
let max_duration = Duration::from_secs(20);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().count() > 0 {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
break;
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(
|
||||
ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the animation needs to continue
|
||||
pub fn cancel(animation_key: &str) -> bool {
|
||||
// should be more than 0
|
||||
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(animation_key);
|
||||
let max_duration = Duration::from_secs(5);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().in_progress(animation_key) {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
ANIMATION_MANAGER.lock().end(animation_key);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(250 / 2));
|
||||
}
|
||||
|
||||
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(animation_key);
|
||||
|
||||
ANIMATION_MANAGER.lock().end_cancel(animation_key);
|
||||
|
||||
latest_cancel_idx == cancel_idx
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
render_dispatcher: (impl RenderDispatcher + Send + 'static),
|
||||
duration: Duration,
|
||||
) -> Result<()> {
|
||||
std::thread::spawn(move || {
|
||||
let animation_key = render_dispatcher.get_animation_key();
|
||||
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
|
||||
let should_animate = Self::cancel(animation_key.as_str());
|
||||
|
||||
if !should_animate {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
render_dispatcher.pre_render()?;
|
||||
|
||||
ANIMATION_MANAGER.lock().start(animation_key.as_str());
|
||||
|
||||
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(animation_key.as_str())
|
||||
{
|
||||
// cancel animation
|
||||
ANIMATION_MANAGER.lock().cancel(animation_key.as_str());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame_start = Instant::now();
|
||||
// calculate progress
|
||||
progress =
|
||||
animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
|
||||
render_dispatcher.render(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(animation_key.as_str());
|
||||
|
||||
// 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_dispatcher.render(progress).ok();
|
||||
}
|
||||
|
||||
render_dispatcher.post_render()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
42
komorebi/src/animation/lerp.rs
Normal file
42
komorebi/src/animation/lerp.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use crate::core::Rect;
|
||||
use crate::AnimationStyle;
|
||||
|
||||
use super::style::apply_ease_func;
|
||||
|
||||
pub trait Lerp<T = Self> {
|
||||
fn lerp(self, end: T, time: f64, style: AnimationStyle) -> T;
|
||||
}
|
||||
|
||||
impl Lerp for i32 {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn lerp(self, end: i32, time: f64, style: AnimationStyle) -> i32 {
|
||||
let time = apply_ease_func(time, style);
|
||||
|
||||
f64::from(end - self).mul_add(time, f64::from(self)).round() as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for f64 {
|
||||
fn lerp(self, end: f64, time: f64, style: AnimationStyle) -> f64 {
|
||||
let time = apply_ease_func(time, style);
|
||||
|
||||
(end - self).mul_add(time, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for u8 {
|
||||
fn lerp(self, end: u8, time: f64, style: AnimationStyle) -> u8 {
|
||||
(self as f64).lerp(end as f64, time, style) as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for Rect {
|
||||
fn lerp(self, end: Rect, time: f64, style: AnimationStyle) -> Rect {
|
||||
Rect {
|
||||
left: self.left.lerp(end.left, time, style),
|
||||
top: self.top.lerp(end.top, time, style),
|
||||
right: self.right.lerp(end.right, time, style),
|
||||
bottom: self.bottom.lerp(end.bottom, time, style),
|
||||
}
|
||||
}
|
||||
}
|
||||
54
komorebi/src/animation/mod.rs
Normal file
54
komorebi/src/animation/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::animation::animation_manager::AnimationManager;
|
||||
use crate::core::animation::AnimationStyle;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use prefix::AnimationPrefix;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
pub use engine::AnimationEngine;
|
||||
pub mod animation_manager;
|
||||
pub mod engine;
|
||||
pub mod lerp;
|
||||
pub mod prefix;
|
||||
pub mod render_dispatcher;
|
||||
pub use render_dispatcher::RenderDispatcher;
|
||||
pub mod style;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum PerAnimationPrefixConfig<T> {
|
||||
Prefix(HashMap<AnimationPrefix, T>),
|
||||
Global(T),
|
||||
}
|
||||
|
||||
pub const DEFAULT_ANIMATION_ENABLED: bool = false;
|
||||
pub const DEFAULT_ANIMATION_STYLE: AnimationStyle = AnimationStyle::Linear;
|
||||
pub const DEFAULT_ANIMATION_DURATION: u64 = 250;
|
||||
pub const DEFAULT_ANIMATION_FPS: u64 = 60;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
|
||||
Arc::new(Mutex::new(AnimationManager::new()));
|
||||
pub static ref ANIMATION_STYLE_GLOBAL: Arc<Mutex<AnimationStyle>> =
|
||||
Arc::new(Mutex::new(DEFAULT_ANIMATION_STYLE));
|
||||
pub static ref ANIMATION_ENABLED_GLOBAL: Arc<AtomicBool> =
|
||||
Arc::new(AtomicBool::new(DEFAULT_ANIMATION_ENABLED));
|
||||
pub static ref ANIMATION_DURATION_GLOBAL: Arc<AtomicU64> =
|
||||
Arc::new(AtomicU64::new(DEFAULT_ANIMATION_DURATION));
|
||||
pub static ref ANIMATION_STYLE_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, AnimationStyle>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref ANIMATION_ENABLED_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, bool>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref ANIMATION_DURATION_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, u64>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(DEFAULT_ANIMATION_FPS);
|
||||
31
komorebi/src/animation/prefix.rs
Normal file
31
komorebi/src/animation/prefix.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use clap::ValueEnum;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AnimationPrefix {
|
||||
Movement,
|
||||
Transparency,
|
||||
}
|
||||
|
||||
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
|
||||
format!("{}:{}", prefix, key)
|
||||
}
|
||||
8
komorebi/src/animation/render_dispatcher.rs
Normal file
8
komorebi/src/animation/render_dispatcher.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
pub trait RenderDispatcher {
|
||||
fn get_animation_key(&self) -> String;
|
||||
fn pre_render(&self) -> Result<()>;
|
||||
fn render(&self, delta: f64) -> Result<()>;
|
||||
fn post_render(&self) -> Result<()>;
|
||||
}
|
||||
@@ -1,22 +1,6 @@
|
||||
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;
|
||||
@@ -370,9 +354,8 @@ impl Ease for EaseInOutBounce {
|
||||
}
|
||||
}
|
||||
}
|
||||
fn apply_ease_func(t: f64) -> f64 {
|
||||
let style = *ANIMATION_STYLE.lock();
|
||||
|
||||
pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
match style {
|
||||
AnimationStyle::Linear => Linear::evaluate(t),
|
||||
AnimationStyle::EaseInSine => EaseInSine::evaluate(t),
|
||||
@@ -406,112 +389,3 @@ fn apply_ease_func(t: f64) -> f64 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,47 +3,86 @@ use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::FOCUS_STATE;
|
||||
use crate::border_manager::RENDER_TARGETS;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use windows::core::PCWSTR;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Foundation::Numerics::Matrix3x2;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::FALSE;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::TRUE;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Gdi::BeginPaint;
|
||||
use windows::Win32::Graphics::Gdi::CreatePen;
|
||||
use windows::Win32::Graphics::Gdi::DeleteObject;
|
||||
use windows::Win32::Graphics::Gdi::EndPaint;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_HWND_RENDER_TARGET_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_PRESENT_OPTIONS_IMMEDIATELY;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
|
||||
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
|
||||
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
|
||||
use windows::Win32::Graphics::Gdi::CreateRectRgn;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::RoundRect;
|
||||
use windows::Win32::Graphics::Gdi::SelectObject;
|
||||
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
|
||||
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
use windows::Win32::Graphics::Gdi::ValidateRect;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows_core::PCWSTR;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
static RENDER_FACTORY: LazyLock<ID2D1Factory> = unsafe {
|
||||
LazyLock::new(|| {
|
||||
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
|
||||
.expect("creating RENDER_FACTORY failed")
|
||||
})
|
||||
};
|
||||
|
||||
static BRUSH_PROPERTIES: LazyLock<D2D1_BRUSH_PROPERTIES> =
|
||||
LazyLock::new(|| D2D1_BRUSH_PROPERTIES {
|
||||
opacity: 1.0,
|
||||
transform: Matrix3x2::identity(),
|
||||
});
|
||||
|
||||
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||
@@ -58,14 +97,36 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
true.into()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Border {
|
||||
pub hwnd: isize,
|
||||
pub render_target: OnceLock<ID2D1HwndRenderTarget>,
|
||||
pub tracking_hwnd: isize,
|
||||
pub window_rect: Rect,
|
||||
pub window_kind: WindowKind,
|
||||
pub style: BorderStyle,
|
||||
pub width: i32,
|
||||
pub offset: i32,
|
||||
pub brush_properties: D2D1_BRUSH_PROPERTIES,
|
||||
pub rounded_rect: D2D1_ROUNDED_RECT,
|
||||
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
|
||||
}
|
||||
|
||||
impl From<isize> for Border {
|
||||
fn from(value: isize) -> Self {
|
||||
Self { hwnd: value }
|
||||
Self {
|
||||
hwnd: value,
|
||||
render_target: OnceLock::new(),
|
||||
tracking_hwnd: 0,
|
||||
window_rect: Rect::default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
style: STYLE.load(),
|
||||
width: BORDER_WIDTH.load(Ordering::Relaxed),
|
||||
offset: BORDER_OFFSET.load(Ordering::Relaxed),
|
||||
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
|
||||
rounded_rect: D2D1_ROUNDED_RECT::default(),
|
||||
brushes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +135,7 @@ impl Border {
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
}
|
||||
|
||||
pub fn create(id: &str) -> color_eyre::Result<Self> {
|
||||
pub fn create(id: &str, tracking_hwnd: isize) -> color_eyre::Result<Self> {
|
||||
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
|
||||
let class_name = PCWSTR(name.as_ptr());
|
||||
|
||||
@@ -83,7 +144,6 @@ impl Border {
|
||||
let window_class = WNDCLASSW {
|
||||
hInstance: h_module.into(),
|
||||
lpszClassName: class_name,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(Self::callback),
|
||||
hbrBackground: WindowsApi::create_solid_brush(0),
|
||||
..Default::default()
|
||||
@@ -91,12 +151,30 @@ impl Border {
|
||||
|
||||
let _ = WindowsApi::register_class_w(&window_class);
|
||||
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
let (border_sender, border_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()), instance)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
let mut border = Self {
|
||||
hwnd: 0,
|
||||
render_target: OnceLock::new(),
|
||||
tracking_hwnd,
|
||||
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
style: STYLE.load(),
|
||||
width: BORDER_WIDTH.load(Ordering::Relaxed),
|
||||
offset: BORDER_OFFSET.load(Ordering::Relaxed),
|
||||
brush_properties: Default::default(),
|
||||
rounded_rect: Default::default(),
|
||||
brushes: HashMap::new(),
|
||||
};
|
||||
|
||||
let border_pointer = std::ptr::addr_of!(border);
|
||||
let hwnd =
|
||||
WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance, border_pointer)?;
|
||||
|
||||
border.hwnd = hwnd;
|
||||
border_sender.send(border_pointer as isize)?;
|
||||
|
||||
let mut msg: MSG = MSG::default();
|
||||
|
||||
@@ -110,42 +188,117 @@ impl Border {
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
hwnd: hwnd_receiver.recv()?,
|
||||
})
|
||||
let border_ref = border_receiver.recv()?;
|
||||
let border = unsafe { &mut *(border_ref as *mut Border) };
|
||||
|
||||
// I have literally no idea, apparently this is to get rid of the black pixels
|
||||
// around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys
|
||||
unsafe {
|
||||
let pos: i32 = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8;
|
||||
let hrgn = CreateRectRgn(pos, 0, pos + 1, 1);
|
||||
let mut bh: DWM_BLURBEHIND = Default::default();
|
||||
if !hrgn.is_invalid() {
|
||||
bh = DWM_BLURBEHIND {
|
||||
dwFlags: DWM_BB_ENABLE | DWM_BB_BLURREGION,
|
||||
fEnable: TRUE,
|
||||
hRgnBlur: hrgn,
|
||||
fTransitionOnMaximized: FALSE,
|
||||
};
|
||||
}
|
||||
|
||||
let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh);
|
||||
}
|
||||
|
||||
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
|
||||
hwnd: HWND(windows_api::as_ptr!(border.hwnd)),
|
||||
pixelSize: Default::default(),
|
||||
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
|
||||
};
|
||||
|
||||
let render_target_properties = D2D1_RENDER_TARGET_PROPERTIES {
|
||||
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
||||
pixelFormat: D2D1_PIXEL_FORMAT {
|
||||
format: DXGI_FORMAT_UNKNOWN,
|
||||
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
|
||||
},
|
||||
dpiX: 96.0,
|
||||
dpiY: 96.0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match unsafe {
|
||||
RENDER_FACTORY
|
||||
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
|
||||
} {
|
||||
Ok(render_target) => unsafe {
|
||||
border.brush_properties = *BRUSH_PROPERTIES.deref();
|
||||
for window_kind in [
|
||||
WindowKind::Single,
|
||||
WindowKind::Stack,
|
||||
WindowKind::Monocle,
|
||||
WindowKind::Unfocused,
|
||||
WindowKind::Floating,
|
||||
] {
|
||||
let color = window_kind_colour(window_kind);
|
||||
let color = D2D1_COLOR_F {
|
||||
r: ((color & 0xFF) as f32) / 255.0,
|
||||
g: (((color >> 8) & 0xFF) as f32) / 255.0,
|
||||
b: (((color >> 16) & 0xFF) as f32) / 255.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
if let Ok(brush) =
|
||||
render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties))
|
||||
{
|
||||
border.brushes.insert(window_kind, brush);
|
||||
}
|
||||
}
|
||||
|
||||
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||
|
||||
if border.render_target.set(render_target.clone()).is_err() {
|
||||
return Err(anyhow!("could not store border render target"));
|
||||
}
|
||||
|
||||
border.rounded_rect = {
|
||||
let radius = 8.0 + border.width as f32 / 2.0;
|
||||
D2D1_ROUNDED_RECT {
|
||||
rect: Default::default(),
|
||||
radiusX: radius,
|
||||
radiusY: radius,
|
||||
}
|
||||
};
|
||||
|
||||
let mut render_targets = RENDER_TARGETS.lock();
|
||||
render_targets.insert(border.hwnd, render_target);
|
||||
Ok(border.clone())
|
||||
},
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
let mut render_targets = RENDER_TARGETS.lock();
|
||||
render_targets.remove(&self.hwnd);
|
||||
WindowsApi::close_window(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn update(&self, rect: &Rect, mut should_invalidate: bool) -> color_eyre::Result<()> {
|
||||
// Make adjustments to the border
|
||||
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
|
||||
let mut rect = *rect;
|
||||
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst));
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
|
||||
rect.add_margin(self.width);
|
||||
rect.add_padding(-self.offset);
|
||||
|
||||
// Update the position of the border if required
|
||||
if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) {
|
||||
WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?;
|
||||
should_invalidate = true;
|
||||
}
|
||||
|
||||
// Invalidate the rect to trigger the callback to update colours etc.
|
||||
if should_invalidate {
|
||||
self.invalidate();
|
||||
}
|
||||
WindowsApi::set_border_pos(self.hwnd, &rect, reference_hwnd)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// this triggers WM_PAINT in the callback below
|
||||
pub fn invalidate(&self) {
|
||||
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
|
||||
}
|
||||
@@ -158,75 +311,208 @@ impl Border {
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_PAINT => {
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
WM_CREATE => {
|
||||
let mut border_pointer: *mut Border =
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
// With the rect that we set in Self::update
|
||||
match WindowsApi::window_rect(window.0 as isize) {
|
||||
Ok(rect) => {
|
||||
// Grab the focus kind for this border
|
||||
let window_kind = {
|
||||
FOCUS_STATE
|
||||
.lock()
|
||||
.get(&(window.0 as isize))
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused)
|
||||
if border_pointer.is_null() {
|
||||
let create_struct: *mut CREATESTRUCTW = lparam.0 as *mut _;
|
||||
border_pointer = (*create_struct).lpCreateParams as *mut _;
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, border_pointer as _);
|
||||
}
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
EVENT_OBJECT_DESTROY => {
|
||||
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
// we don't actually want to destroy the window here, just hide it for quicker
|
||||
// visual feedback to the user; the actual destruction will be handled by the
|
||||
// core border manager loop
|
||||
WindowsApi::hide_window(window.0 as isize);
|
||||
LRESULT(0)
|
||||
}
|
||||
EVENT_OBJECT_LOCATIONCHANGE => {
|
||||
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let reference_hwnd = lparam.0;
|
||||
|
||||
let old_rect = (*border_pointer).window_rect;
|
||||
let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default();
|
||||
|
||||
(*border_pointer).window_rect = rect;
|
||||
|
||||
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if !rect.is_same_size_as(&old_rect) {
|
||||
if let Some(render_target) = (*border_pointer).render_target.get() {
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
// Set up the brush to draw the border
|
||||
let hpen = CreatePen(
|
||||
PS_SOLID | PS_INSIDEFRAME,
|
||||
BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
COLORREF(window_kind_colour(window_kind)),
|
||||
);
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let hbrush = WindowsApi::create_solid_brush(0);
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Draw the border
|
||||
SelectObject(hdc, hpen);
|
||||
SelectObject(hdc, hbrush);
|
||||
// TODO(raggi): this is approximately the correct curvature for
|
||||
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
|
||||
// often the bottom right has a different shape. Furthermore if
|
||||
// the window was made with DWMWCP_ROUNDSMALL then this is the
|
||||
// wrong size. In the future we should read the DWM properties
|
||||
// of windows and attempt to match appropriately.
|
||||
match STYLE.load() {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
// TODO: error handling
|
||||
let _ =
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
} else {
|
||||
// TODO: error handling
|
||||
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
BorderStyle::Rounded => {
|
||||
// TODO: error handling
|
||||
let _ = RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
// TODO: error handling
|
||||
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hpen);
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hbrush);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("could not get border rect: {}", error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: error handling
|
||||
let _ = EndPaint(window, &ps);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_PAINT => {
|
||||
if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
|
||||
let border_pointer: *mut Border =
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||
|
||||
// Update position to update the ZOrder
|
||||
let border_window_rect = (*border_pointer).window_rect;
|
||||
|
||||
tracing::trace!("updating border position");
|
||||
if let Err(error) =
|
||||
(*border_pointer).set_position(&border_window_rect, reference_hwnd)
|
||||
{
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if let Some(render_target) = (*border_pointer).render_target.get() {
|
||||
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
||||
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
||||
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
// Get window kind and color
|
||||
|
||||
(*border_pointer).window_kind = FOCUS_STATE
|
||||
.lock()
|
||||
.get(&(window.0 as isize))
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused);
|
||||
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
(*border_pointer).style = STYLE.load();
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = ValidateRect(window, None);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod border;
|
||||
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::WindowKind;
|
||||
@@ -12,7 +11,7 @@ use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
use border::Border;
|
||||
pub use border::Border;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
@@ -30,15 +29,14 @@ use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
|
||||
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);
|
||||
@@ -56,7 +54,10 @@ lazy_static! {
|
||||
lazy_static! {
|
||||
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
|
||||
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
|
||||
static ref WINDOWS_BORDERS: Mutex<HashMap<isize, Border>> = Mutex::new(HashMap::new());
|
||||
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
|
||||
static ref RENDER_TARGETS: Mutex<HashMap<isize, ID2D1HwndRenderTarget>> =
|
||||
Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub struct Notification(pub Option<isize>);
|
||||
@@ -75,6 +76,10 @@ fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn window_border(hwnd: isize) -> Option<Border> {
|
||||
WINDOWS_BORDERS.lock().get(&hwnd).cloned()
|
||||
}
|
||||
|
||||
pub fn send_notification(hwnd: Option<isize>) {
|
||||
if event_tx().try_send(Notification(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
@@ -89,12 +94,13 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
);
|
||||
|
||||
for (_, border) in borders.iter() {
|
||||
border.destroy()?;
|
||||
let _ = border.destroy();
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
BORDERS_MONITORS.lock().clear();
|
||||
FOCUS_STATE.lock().clear();
|
||||
RENDER_TARGETS.lock().clear();
|
||||
|
||||
let mut remaining_hwnds = vec![];
|
||||
|
||||
@@ -107,7 +113,7 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
tracing::info!("purging unknown borders: {:?}", remaining_hwnds);
|
||||
|
||||
for hwnd in remaining_hwnds {
|
||||
Border::from(hwnd).destroy()?;
|
||||
let _ = Border::from(hwnd).destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,11 +122,11 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
|
||||
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),
|
||||
WindowKind::Unfocused => UNFOCUSED.load(Ordering::Relaxed),
|
||||
WindowKind::Single => FOCUSED.load(Ordering::Relaxed),
|
||||
WindowKind::Stack => STACK.load(Ordering::Relaxed),
|
||||
WindowKind::Monocle => MONOCLE.load(Ordering::Relaxed),
|
||||
WindowKind::Floating => FLOATING.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +146,6 @@ 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(None))?;
|
||||
|
||||
@@ -157,13 +162,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
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 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<_>>();
|
||||
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
|
||||
|
||||
drop(state);
|
||||
|
||||
@@ -233,10 +239,19 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
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())
|
||||
{
|
||||
// when we switch focus to/from a floating window
|
||||
let switch_focus_to_from_floating_window = floating_window_hwnds.iter().any(|fw| {
|
||||
// if we switch focus to a floating window
|
||||
fw == ¬ification.0.unwrap_or_default() ||
|
||||
// if there is any floating window with a `WindowKind::Floating` border
|
||||
// that no longer is the foreground window then we need to update that
|
||||
// border.
|
||||
(fw != &foreground_window
|
||||
&& window_border(*fw)
|
||||
.map(|b| b.window_kind == WindowKind::Floating)
|
||||
.unwrap_or_default())
|
||||
});
|
||||
if !should_process_notification && switch_focus_to_from_floating_window {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
@@ -255,11 +270,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
let mut borders_monitors = BORDERS_MONITORS.lock();
|
||||
let mut windows_borders = WINDOWS_BORDERS.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
|
||||
@@ -271,6 +285,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
windows_borders.clear();
|
||||
|
||||
previous_is_paused = is_paused;
|
||||
continue 'receiver;
|
||||
@@ -300,10 +315,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let mut new_border = false;
|
||||
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()) {
|
||||
if let Ok(border) = Border::create(
|
||||
monocle.id(),
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd,
|
||||
) {
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
@@ -311,25 +331,33 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
|
||||
let new_focus_state = if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
};
|
||||
border.window_kind = new_focus_state;
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(
|
||||
border.hwnd,
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
},
|
||||
);
|
||||
focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
let reference_hwnd =
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd;
|
||||
|
||||
border.update(&rect, true)?;
|
||||
let rect = WindowsApi::window_rect(reference_hwnd)?;
|
||||
|
||||
if new_border {
|
||||
border.set_position(&rect, reference_hwnd)?;
|
||||
}
|
||||
|
||||
border.invalidate();
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
windows_borders.insert(
|
||||
monocle.focused_window().cloned().unwrap_or_default().hwnd,
|
||||
border.clone(),
|
||||
);
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
let mut to_remove = vec![];
|
||||
@@ -350,9 +378,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let is_maximized = WindowsApi::is_zoomed(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
);
|
||||
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let foreground_monitor_id =
|
||||
WindowsApi::monitor_from_window(foreground_hwnd);
|
||||
let is_maximized = foreground_monitor_id == m.id()
|
||||
&& WindowsApi::is_zoomed(foreground_hwnd);
|
||||
|
||||
if is_maximized {
|
||||
let mut to_remove = vec![];
|
||||
@@ -398,53 +428,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
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 mut new_border = false;
|
||||
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()) {
|
||||
if let Ok(border) = Border::create(
|
||||
c.id(),
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
) {
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
@@ -452,13 +445,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
|| c.focused_window()
|
||||
.map(|w| w.hwnd != foreground_window)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
} else if c.windows().len() > 1 {
|
||||
@@ -466,6 +460,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
} else {
|
||||
WindowKind::Single
|
||||
};
|
||||
border.window_kind = new_focus_state;
|
||||
|
||||
// Update the focused state for all containers on this workspace
|
||||
{
|
||||
@@ -473,61 +468,41 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
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 reference_hwnd =
|
||||
c.focused_window().copied().unwrap_or_default().hwnd;
|
||||
|
||||
let rect = WindowsApi::window_rect(reference_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)?;
|
||||
if new_border {
|
||||
border.set_position(&rect, reference_hwnd)?;
|
||||
}
|
||||
|
||||
if should_invalidate {
|
||||
border.invalidate();
|
||||
}
|
||||
|
||||
borders_monitors.insert(c.id().clone(), monitor_idx);
|
||||
windows_borders.insert(
|
||||
c.focused_window().cloned().unwrap_or_default().hwnd,
|
||||
border.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let restore_z_order = Z_ORDER.load();
|
||||
Z_ORDER.store(ZOrder::TopMost);
|
||||
|
||||
'windows: for window in ws.floating_windows() {
|
||||
let hwnd = window.hwnd;
|
||||
let notification_hwnd = notification.0.unwrap_or_default();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
for window in ws.floating_windows() {
|
||||
let mut new_border = false;
|
||||
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())
|
||||
if let Ok(border) =
|
||||
Border::create(&window.hwnd.to_string(), window.hwnd)
|
||||
{
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
@@ -535,32 +510,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
};
|
||||
|
||||
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 new_focus_state = WindowKind::Unfocused;
|
||||
|
||||
if foreground_window == window.hwnd {
|
||||
new_focus_state = WindowKind::Floating;
|
||||
}
|
||||
|
||||
border.window_kind = new_focus_state;
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
last_focus_state =
|
||||
@@ -574,10 +532,17 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
if new_border {
|
||||
border.set_position(&rect, window.hwnd)?;
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
if should_invalidate {
|
||||
border.invalidate();
|
||||
}
|
||||
|
||||
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
|
||||
windows_borders.insert(window.hwnd, border.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,18 @@ impl From<Color32> for Colour {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Colour> for Color32 {
|
||||
fn from(value: Colour) -> Self {
|
||||
match value {
|
||||
Colour::Rgb(rgb) => Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8),
|
||||
Colour::Hex(hex) => {
|
||||
let rgb = Rgb::from(hex);
|
||||
Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct Hex(HexColor);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ impl<'a, T: Clone> ComIn<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Deref for ComIn<'a, T> {
|
||||
impl<T> Deref for ComIn<'_, T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
|
||||
@@ -75,6 +75,18 @@ impl Container {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
|
||||
for (idx, window) in self.windows().iter().enumerate() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn contains_window(&self, hwnd: isize) -> bool {
|
||||
for window in self.windows() {
|
||||
if window.hwnd == hwnd {
|
||||
|
||||
@@ -14,6 +14,8 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
use crate::KomorebiTheme;
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
@@ -48,6 +50,7 @@ pub enum SocketMessage {
|
||||
StackWindow(OperationDirection),
|
||||
UnstackWindow,
|
||||
CycleStack(CycleDirection),
|
||||
CycleStackIndex(CycleDirection),
|
||||
FocusStackWindow(usize),
|
||||
StackAll,
|
||||
UnstackAll,
|
||||
@@ -74,6 +77,7 @@ pub enum SocketMessage {
|
||||
Promote,
|
||||
PromoteFocus,
|
||||
PromoteWindow(OperationDirection),
|
||||
EagerFocus(String),
|
||||
ToggleFloat,
|
||||
ToggleMonocle,
|
||||
ToggleMaximize,
|
||||
@@ -102,6 +106,7 @@ pub enum SocketMessage {
|
||||
NewWorkspace,
|
||||
ToggleTiling,
|
||||
Stop,
|
||||
StopIgnoreRestore,
|
||||
TogglePause,
|
||||
Retile,
|
||||
RetileWithResizeDimensions,
|
||||
@@ -113,6 +118,7 @@ pub enum SocketMessage {
|
||||
CycleFocusWorkspace(CycleDirection),
|
||||
FocusMonitorNumber(usize),
|
||||
FocusLastWorkspace,
|
||||
CloseWorkspace,
|
||||
FocusWorkspaceNumber(usize),
|
||||
FocusWorkspaceNumbers(usize),
|
||||
FocusMonitorWorkspaceNumber(usize, usize),
|
||||
@@ -143,10 +149,11 @@ pub enum SocketMessage {
|
||||
WatchConfiguration(bool),
|
||||
CompleteConfiguration,
|
||||
AltFocusHack(bool),
|
||||
Animation(bool),
|
||||
AnimationDuration(u64),
|
||||
Theme(KomorebiTheme),
|
||||
Animation(bool, Option<AnimationPrefix>),
|
||||
AnimationDuration(u64, Option<AnimationPrefix>),
|
||||
AnimationFps(u64),
|
||||
AnimationStyle(AnimationStyle),
|
||||
AnimationStyle(AnimationStyle, Option<AnimationPrefix>),
|
||||
#[serde(alias = "ActiveWindowBorder")]
|
||||
Border(bool),
|
||||
#[serde(alias = "ActiveWindowBorderColour")]
|
||||
@@ -179,6 +186,7 @@ pub enum SocketMessage {
|
||||
ClearWorkspaceRules(usize, usize),
|
||||
ClearNamedWorkspaceRules(String),
|
||||
ClearAllWorkspaceRules,
|
||||
EnforceWorkspaceRules,
|
||||
#[serde(alias = "FloatRule")]
|
||||
IgnoreRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
@@ -301,6 +309,8 @@ pub enum BorderImplementation {
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
)]
|
||||
pub enum WindowKind {
|
||||
Single,
|
||||
|
||||
@@ -37,6 +37,12 @@ impl From<Rect> for RECT {
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||
self.right == rhs.right && self.bottom == rhs.bottom
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// decrease the size of self by the padding amount.
|
||||
pub fn add_padding<T>(&mut self, padding: T)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
pub mod animation;
|
||||
pub mod animation_manager;
|
||||
pub mod border_manager;
|
||||
pub mod com;
|
||||
#[macro_use]
|
||||
@@ -20,6 +19,7 @@ pub mod set_window_position;
|
||||
pub mod stackbar_manager;
|
||||
pub mod static_config;
|
||||
pub mod styles;
|
||||
pub mod theme_manager;
|
||||
pub mod transparency_manager;
|
||||
pub mod window;
|
||||
pub mod window_manager;
|
||||
@@ -46,8 +46,6 @@ 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::*;
|
||||
@@ -215,15 +213,9 @@ 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 NO_TITLEBAR: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
@@ -237,8 +229,6 @@ 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);
|
||||
|
||||
@@ -300,10 +290,14 @@ pub struct Notification {
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
|
||||
let is_subscription_event = matches!(
|
||||
let is_override_event = matches!(
|
||||
notification.event,
|
||||
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
|
||||
| NotificationEvent::Socket(SocketMessage::AddSubscriberSocketWithOptions(_, _))
|
||||
| NotificationEvent::Socket(SocketMessage::Theme(_))
|
||||
| NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(_))
|
||||
| NotificationEvent::WindowManager(WindowManagerEvent::TitleUpdate(_, _))
|
||||
| NotificationEvent::WindowManager(WindowManagerEvent::Show(_, _))
|
||||
);
|
||||
|
||||
let notification = &serde_json::to_string(¬ification)?;
|
||||
@@ -318,7 +312,7 @@ pub fn notify_subscribers(notification: Notification, state_has_been_modified: b
|
||||
.unwrap_or_default()
|
||||
.filter_state_changes;
|
||||
|
||||
if !apply_state_filter || state_has_been_modified || is_subscription_event {
|
||||
if !apply_state_filter || state_has_been_modified || is_override_event {
|
||||
match UnixStream::connect(path) {
|
||||
Ok(mut stream) => {
|
||||
tracing::debug!("pushed notification to subscriber: {socket}");
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
clippy::doc_markdown
|
||||
)]
|
||||
|
||||
use std::env::temp_dir;
|
||||
use std::net::Shutdown;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -17,6 +18,9 @@ use std::time::Duration;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_utils::Backoff;
|
||||
use komorebi::animation::AnimationEngine;
|
||||
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
@@ -38,7 +42,9 @@ use komorebi::process_movement::listen_for_movements;
|
||||
use komorebi::reaper;
|
||||
use komorebi::stackbar_manager;
|
||||
use komorebi::static_config::StaticConfig;
|
||||
use komorebi::theme_manager;
|
||||
use komorebi::transparency_manager;
|
||||
use komorebi::window_manager::State;
|
||||
use komorebi::window_manager::WindowManager;
|
||||
use komorebi::windows_api::WindowsApi;
|
||||
use komorebi::winevent_listener;
|
||||
@@ -152,6 +158,9 @@ struct Opts {
|
||||
/// Path to a static configuration JSON file
|
||||
#[clap(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
|
||||
#[clap(long)]
|
||||
clean_state: bool,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -256,6 +265,13 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let dumped_state = temp_dir().join("komorebi.state.json");
|
||||
|
||||
if !opts.clean_state && dumped_state.is_file() {
|
||||
let state: State = serde_json::from_str(&std::fs::read_to_string(&dumped_state)?)?;
|
||||
wm.lock().apply_state(state);
|
||||
}
|
||||
|
||||
wm.lock().retile_all(false)?;
|
||||
|
||||
listen_for_events(wm.clone());
|
||||
@@ -271,6 +287,7 @@ fn main() -> Result<()> {
|
||||
monitor_reconciliator::listen_for_notifications(wm.clone())?;
|
||||
reaper::watch_for_orphans(wm.clone());
|
||||
focus_manager::listen_for_notifications(wm.clone());
|
||||
theme_manager::listen_for_notifications();
|
||||
|
||||
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
||||
ctrlc::set_handler(move || {
|
||||
@@ -285,7 +302,13 @@ fn main() -> Result<()> {
|
||||
|
||||
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
|
||||
|
||||
wm.lock().restore_all_windows()?;
|
||||
let state = State::from(&*wm.lock());
|
||||
std::fs::write(dumped_state, serde_json::to_string_pretty(&state)?)?;
|
||||
|
||||
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
|
||||
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
|
||||
wm.lock().restore_all_windows(false)?;
|
||||
AnimationEngine::wait_for_all_animations();
|
||||
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
|
||||
@@ -139,6 +139,86 @@ impl Monitor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a container to this `Monitor` using the move direction to calculate if the container
|
||||
/// should be added in front of all containers, in the back or in place of the focused
|
||||
/// container, moving the rest along. The move direction should be from the origin monitor
|
||||
/// towards the target monitor or from the origin workspace towards the target workspace.
|
||||
pub fn add_container_with_direction(
|
||||
&mut self,
|
||||
container: Container,
|
||||
workspace_idx: Option<usize>,
|
||||
direction: OperationDirection,
|
||||
) -> Result<()> {
|
||||
let workspace = if let Some(idx) = workspace_idx {
|
||||
self.workspaces_mut()
|
||||
.get_mut(idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
|
||||
} else {
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
};
|
||||
|
||||
match direction {
|
||||
OperationDirection::Left => {
|
||||
// insert the container into the workspace on the monitor at the back (or rightmost position)
|
||||
// if we are moving across a boundary to the left (back = right side of the target)
|
||||
match workspace.layout() {
|
||||
Layout::Default(layout) => match layout {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
workspace.add_container_to_front(container);
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack => {
|
||||
if workspace.containers().len() == 1 {
|
||||
workspace.insert_container_at_idx(0, container);
|
||||
} else {
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
},
|
||||
Layout::Custom(_) => {
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
// insert the container into the workspace on the monitor at the front (or leftmost position)
|
||||
// if we are moving across a boundary to the right (front = left side of the target)
|
||||
match workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index = layout.leftmost_index(workspace.containers().len());
|
||||
|
||||
match layout {
|
||||
DefaultLayout::RightMainVerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => {
|
||||
if workspace.containers().len() == 1 {
|
||||
workspace.add_container_to_back(container);
|
||||
} else {
|
||||
workspace.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
workspace.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
workspace.add_container_to_front(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
OperationDirection::Up | OperationDirection::Down => {
|
||||
// insert the container into the workspace on the monitor at the position
|
||||
// where the currently focused container on that workspace is
|
||||
workspace.insert_container_at_idx(workspace.focused_container_idx(), container);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_workspace_by_idx(&mut self, idx: usize) -> Option<Workspace> {
|
||||
if idx < self.workspaces().len() {
|
||||
return self.workspaces_mut().remove(idx);
|
||||
@@ -215,54 +295,14 @@ impl Monitor {
|
||||
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);
|
||||
}
|
||||
if let Some(direction) = direction {
|
||||
self.add_container_with_direction(
|
||||
container,
|
||||
Some(target_workspace_idx),
|
||||
direction,
|
||||
)?;
|
||||
} else {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
);
|
||||
|
||||
ACTIVE.store(true, Ordering::SeqCst);
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
|
||||
continue 'receiver;
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
@@ -22,6 +21,9 @@ use schemars::gen::SchemaSettings;
|
||||
use schemars::schema_for;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
|
||||
use crate::core::config_generation::ApplicationConfiguration;
|
||||
use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
@@ -40,6 +42,10 @@ use crate::core::StateQuery;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowKind;
|
||||
|
||||
use crate::animation::ANIMATION_DURATION_GLOBAL;
|
||||
use crate::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use crate::animation::ANIMATION_FPS;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::IMPLEMENTATION;
|
||||
use crate::border_manager::STYLE;
|
||||
@@ -51,6 +57,7 @@ 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::theme_manager;
|
||||
use crate::transparency_manager;
|
||||
use crate::window::RuleDebug;
|
||||
use crate::window::Window;
|
||||
@@ -58,14 +65,11 @@ use crate::window_manager;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener;
|
||||
use crate::workspace::WorkspaceWindowLocation;
|
||||
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;
|
||||
@@ -108,10 +112,19 @@ pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
|
||||
tracing::info!("listening on komorebi.sock");
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(stream) => match read_commands_uds(&wm, stream) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
},
|
||||
Ok(stream) => {
|
||||
let wm_clone = wm.clone();
|
||||
std::thread::spawn(move || {
|
||||
match stream.set_read_timeout(Some(Duration::from_secs(1))) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
match read_commands_uds(&wm_clone, stream) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error);
|
||||
break;
|
||||
@@ -217,6 +230,65 @@ impl WindowManager {
|
||||
self.focus_container_in_direction(direction)?;
|
||||
self.promote_container_to_front()?
|
||||
}
|
||||
SocketMessage::EagerFocus(ref exe) => {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx = self.focused_workspace_idx()?;
|
||||
|
||||
let mut window_location = None;
|
||||
let mut monitor_workspace_indices = None;
|
||||
|
||||
'search: for (monitor_idx, monitor) in self.monitors().iter().enumerate() {
|
||||
for (workspace_idx, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
if let Some(location) = workspace.location_from_exe(exe) {
|
||||
window_location = Some(location);
|
||||
monitor_workspace_indices = Some((monitor_idx, workspace_idx));
|
||||
break 'search;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((monitor_idx, workspace_idx)) = monitor_workspace_indices {
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
|
||||
if workspace_idx != focused_workspace_idx {
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(location) = window_location {
|
||||
match location {
|
||||
WorkspaceWindowLocation::Monocle(window_idx) => {
|
||||
self.focus_container_window(window_idx)?;
|
||||
}
|
||||
WorkspaceWindowLocation::Maximized => {
|
||||
if let Some(window) =
|
||||
self.focused_workspace_mut()?.maximized_window_mut()
|
||||
{
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
WorkspaceWindowLocation::Container(container_idx, window_idx) => {
|
||||
let focused_container_idx = self.focused_container_idx()?;
|
||||
if container_idx != focused_container_idx {
|
||||
self.focused_workspace_mut()?.focus_container(container_idx);
|
||||
}
|
||||
|
||||
self.focus_container_window(window_idx)?;
|
||||
}
|
||||
WorkspaceWindowLocation::Floating(window_idx) => {
|
||||
if let Some(window) = self
|
||||
.focused_workspace_mut()?
|
||||
.floating_windows_mut()
|
||||
.get_mut(window_idx)
|
||||
{
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusWindow(direction) => {
|
||||
self.focus_container_in_direction(direction)?;
|
||||
}
|
||||
@@ -237,6 +309,10 @@ impl WindowManager {
|
||||
self.cycle_container_window_in_direction(direction)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
SocketMessage::CycleStackIndex(direction) => {
|
||||
self.cycle_container_window_index_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
|
||||
@@ -380,6 +456,13 @@ impl WindowManager {
|
||||
let mut workspace_rules = WORKSPACE_MATCHING_RULES.lock();
|
||||
workspace_rules.clear();
|
||||
}
|
||||
SocketMessage::EnforceWorkspaceRules => {
|
||||
{
|
||||
let mut already_moved = self.already_moved_window_handles.lock();
|
||||
already_moved.clear();
|
||||
}
|
||||
self.enforce_workspace_rules()?;
|
||||
}
|
||||
SocketMessage::ManageRule(identifier, ref id) => {
|
||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
|
||||
@@ -518,7 +601,8 @@ impl WindowManager {
|
||||
self.move_container_to_workspace(workspace_idx, true, None)?;
|
||||
}
|
||||
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, None, true)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
|
||||
}
|
||||
SocketMessage::SwapWorkspacesToMonitorNumber(monitor_idx) => {
|
||||
self.swap_focused_monitor(monitor_idx)?;
|
||||
@@ -530,7 +614,8 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
|
||||
);
|
||||
|
||||
self.move_container_to_monitor(monitor_idx, None, true)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
|
||||
}
|
||||
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
|
||||
self.move_container_to_workspace(workspace_idx, false, None)?;
|
||||
@@ -552,7 +637,8 @@ impl WindowManager {
|
||||
self.move_container_to_workspace(workspace_idx, false, None)?;
|
||||
}
|
||||
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, None, false)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
|
||||
}
|
||||
SocketMessage::CycleSendContainerToMonitor(direction) => {
|
||||
let monitor_idx = direction.next_idx(
|
||||
@@ -561,22 +647,37 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
|
||||
);
|
||||
|
||||
self.move_container_to_monitor(monitor_idx, None, false)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
|
||||
}
|
||||
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(
|
||||
monitor_idx,
|
||||
Option::from(workspace_idx),
|
||||
false,
|
||||
direction,
|
||||
)?;
|
||||
}
|
||||
SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(
|
||||
monitor_idx,
|
||||
Option::from(workspace_idx),
|
||||
true,
|
||||
direction,
|
||||
)?;
|
||||
}
|
||||
SocketMessage::SendContainerToNamedWorkspace(ref workspace) => {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
self.monitor_workspace_index_by_name(workspace)
|
||||
{
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(
|
||||
monitor_idx,
|
||||
Option::from(workspace_idx),
|
||||
false,
|
||||
direction,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -584,7 +685,13 @@ impl WindowManager {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
self.monitor_workspace_index_by_name(workspace)
|
||||
{
|
||||
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(
|
||||
monitor_idx,
|
||||
Option::from(workspace_idx),
|
||||
true,
|
||||
direction,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,12 +735,10 @@ 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)?
|
||||
}
|
||||
SocketMessage::RetileWithResizeDimensions => {
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
border_manager::destroy_all_borders()?;
|
||||
self.retile_all(true)?
|
||||
}
|
||||
@@ -760,6 +865,42 @@ impl WindowManager {
|
||||
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
SocketMessage::CloseWorkspace => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
|
||||
let mut can_close = false;
|
||||
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
let next_focused_workspace_idx = focused_workspace_idx.saturating_sub(1);
|
||||
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if monitor.workspaces().len() > 1
|
||||
&& workspace.containers().is_empty()
|
||||
&& workspace.floating_windows().is_empty()
|
||||
&& workspace.monocle_container().is_none()
|
||||
&& workspace.maximized_window().is_none()
|
||||
&& workspace.name().is_none()
|
||||
{
|
||||
can_close = true;
|
||||
}
|
||||
}
|
||||
|
||||
if can_close
|
||||
&& monitor
|
||||
.workspaces_mut()
|
||||
.remove(focused_workspace_idx)
|
||||
.is_some()
|
||||
{
|
||||
self.focus_workspace(next_focused_workspace_idx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusLastWorkspace => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
@@ -834,26 +975,10 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
SocketMessage::Stop => {
|
||||
tracing::info!(
|
||||
"received stop command, restoring all hidden windows and terminating process"
|
||||
);
|
||||
self.restore_all_windows()?;
|
||||
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
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)
|
||||
self.stop(false)?;
|
||||
}
|
||||
SocketMessage::StopIgnoreRestore => {
|
||||
self.stop(true)?;
|
||||
}
|
||||
SocketMessage::MonitorIndexPreference(index_preference, left, top, right, bottom) => {
|
||||
let mut monitor_index_preferences = MONITOR_INDEX_PREFERENCES.lock();
|
||||
@@ -1168,7 +1293,7 @@ impl WindowManager {
|
||||
// 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()?;
|
||||
self.restore_all_windows(false)?;
|
||||
|
||||
// Create a new wm from the config path
|
||||
let mut wm = StaticConfig::preload(
|
||||
@@ -1434,6 +1559,16 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::Border(enable) => {
|
||||
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
|
||||
if !enable {
|
||||
match IMPLEMENTATION.load() {
|
||||
BorderImplementation::Komorebi => {
|
||||
border_manager::destroy_all_borders()?;
|
||||
}
|
||||
BorderImplementation::Windows => {
|
||||
self.remove_all_accents()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::BorderImplementation(implementation) => {
|
||||
if !*WINDOWS_11 && matches!(implementation, BorderImplementation::Windows) {
|
||||
@@ -1480,18 +1615,41 @@ 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::Animation(enable, prefix) => match prefix {
|
||||
Some(prefix) => {
|
||||
ANIMATION_ENABLED_PER_ANIMATION
|
||||
.lock()
|
||||
.insert(prefix, enable);
|
||||
}
|
||||
None => {
|
||||
ANIMATION_ENABLED_GLOBAL.store(enable, Ordering::SeqCst);
|
||||
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
},
|
||||
SocketMessage::AnimationDuration(duration, prefix) => match prefix {
|
||||
Some(prefix) => {
|
||||
ANIMATION_DURATION_PER_ANIMATION
|
||||
.lock()
|
||||
.insert(prefix, duration);
|
||||
}
|
||||
None => {
|
||||
ANIMATION_DURATION_GLOBAL.store(duration, Ordering::SeqCst);
|
||||
ANIMATION_DURATION_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
},
|
||||
SocketMessage::AnimationFps(fps) => {
|
||||
ANIMATION_FPS.store(fps, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::AnimationStyle(style) => {
|
||||
*ANIMATION_STYLE.lock() = style;
|
||||
}
|
||||
SocketMessage::AnimationStyle(style, prefix) => match prefix {
|
||||
Some(prefix) => {
|
||||
ANIMATION_STYLE_PER_ANIMATION.lock().insert(prefix, style);
|
||||
}
|
||||
None => {
|
||||
let mut animation_style = ANIMATION_STYLE_GLOBAL.lock();
|
||||
*animation_style = style;
|
||||
ANIMATION_STYLE_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
},
|
||||
SocketMessage::ToggleTransparency => {
|
||||
let current = transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst);
|
||||
transparency_manager::TRANSPARENCY_ENABLED.store(!current, Ordering::SeqCst);
|
||||
@@ -1569,10 +1727,24 @@ impl WindowManager {
|
||||
|
||||
reply.write_all(config.as_bytes())?;
|
||||
}
|
||||
SocketMessage::RemoveTitleBar(_, ref id) => {
|
||||
SocketMessage::RemoveTitleBar(identifier, ref id) => {
|
||||
let mut identifiers = NO_TITLEBAR.lock();
|
||||
if !identifiers.contains(id) {
|
||||
identifiers.push(id.clone());
|
||||
|
||||
let mut should_push = true;
|
||||
for i in &*identifiers {
|
||||
if let MatchingRule::Simple(i) = i {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleTitleBars => {
|
||||
@@ -1588,6 +1760,9 @@ impl WindowManager {
|
||||
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
theme_manager::send_notification(theme);
|
||||
}
|
||||
// Deprecated commands
|
||||
SocketMessage::AltFocusHack(_)
|
||||
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
|
||||
|
||||
@@ -228,8 +228,7 @@ impl WindowManager {
|
||||
.is_some();
|
||||
|
||||
if !window.is_window()
|
||||
|| should_act
|
||||
|| !programmatically_hidden_hwnds.contains(&window.hwnd)
|
||||
|| (should_act && !programmatically_hidden_hwnds.contains(&window.hwnd))
|
||||
{
|
||||
hide = true;
|
||||
}
|
||||
@@ -279,158 +278,191 @@ impl WindowManager {
|
||||
WindowManagerEvent::Show(_, window)
|
||||
| WindowManagerEvent::Manage(window)
|
||||
| WindowManagerEvent::Uncloak(_, window) => {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
if matches!(event, WindowManagerEvent::Uncloak(_, _))
|
||||
&& self.uncloack_to_ignore >= 1
|
||||
{
|
||||
tracing::info!("ignoring uncloak after monocle move by mouse across monitors");
|
||||
self.uncloack_to_ignore = self.uncloack_to_ignore.saturating_sub(1);
|
||||
} else {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
let mut needs_reconciliation = false;
|
||||
let mut needs_reconciliation = false;
|
||||
|
||||
for (i, monitors) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitors.workspaces().iter().enumerate() {
|
||||
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
|
||||
// At this point we know we are going to send a notification to the workspace reconciliator
|
||||
// So we get the topmost window returned by EnumWindows, which is almost always the window
|
||||
// that has been selected by alt-tab
|
||||
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
|
||||
if let Some(first) =
|
||||
alt_tab_windows.iter().find(|w| w.title().is_ok())
|
||||
for (i, monitors) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitors.workspaces().iter().enumerate() {
|
||||
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
|
||||
// At this point we know we are going to send a notification to the workspace reconciliator
|
||||
// So we get the topmost window returned by EnumWindows, which is almost always the window
|
||||
// that has been selected by alt-tab
|
||||
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
|
||||
if let Some(first) =
|
||||
alt_tab_windows.iter().find(|w| w.title().is_ok())
|
||||
{
|
||||
// If our record of this HWND hasn't been updated in over a minute
|
||||
let mut instant = ALT_TAB_HWND_INSTANT.lock();
|
||||
if instant.elapsed().gt(&Duration::from_secs(1)) {
|
||||
// Update our record with the HWND we just found
|
||||
ALT_TAB_HWND.store(Some(first.hwnd));
|
||||
// Update the timestamp of our record
|
||||
*instant = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace_reconciliator::send_notification(i, j);
|
||||
needs_reconciliation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There are some applications such as Firefox where, if they are focused when a
|
||||
// workspace switch takes place, it will fire an additional Show event, which will
|
||||
// result in them being associated with both the original workspace and the workspace
|
||||
// being switched to. This loop is to try to ensure that we don't end up with
|
||||
// duplicates across multiple workspaces, as it results in ghost layout tiles.
|
||||
let mut proceed = true;
|
||||
|
||||
for (i, monitor) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
if workspace.contains_window(window.hwnd)
|
||||
&& i != self.focused_monitor_idx()
|
||||
&& j != monitor.focused_workspace_idx()
|
||||
{
|
||||
tracing::debug!(
|
||||
"ignoring show event for window already associated with another workspace"
|
||||
);
|
||||
|
||||
window.hide();
|
||||
proceed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if proceed {
|
||||
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 {
|
||||
let floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
let mut should_float = false;
|
||||
|
||||
if !floating_applications.is_empty() {
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) =
|
||||
(window.title(), window.exe(), window.class(), window.path())
|
||||
{
|
||||
// If our record of this HWND hasn't been updated in over a minute
|
||||
let mut instant = ALT_TAB_HWND_INSTANT.lock();
|
||||
if instant.elapsed().gt(&Duration::from_secs(1)) {
|
||||
// Update our record with the HWND we just found
|
||||
ALT_TAB_HWND.store(Some(first.hwnd));
|
||||
// Update the timestamp of our record
|
||||
*instant = Instant::now();
|
||||
should_float = should_act(
|
||||
&title,
|
||||
&exe_name,
|
||||
&class,
|
||||
&path,
|
||||
&floating_applications,
|
||||
®ex_identifiers,
|
||||
)
|
||||
.is_some();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace_reconciliator::send_notification(i, j);
|
||||
needs_reconciliation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There are some applications such as Firefox where, if they are focused when a
|
||||
// workspace switch takes place, it will fire an additional Show event, which will
|
||||
// result in them being associated with both the original workspace and the workspace
|
||||
// being switched to. This loop is to try to ensure that we don't end up with
|
||||
// duplicates across multiple workspaces, as it results in ghost layout tiles.
|
||||
let mut proceed = true;
|
||||
|
||||
for (i, monitor) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
if workspace.container_for_window(window.hwnd).is_some()
|
||||
&& i != self.focused_monitor_idx()
|
||||
&& j != monitor.focused_workspace_idx()
|
||||
{
|
||||
tracing::debug!(
|
||||
"ignoring show event for window already associated with another workspace"
|
||||
);
|
||||
|
||||
window.hide();
|
||||
proceed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if proceed {
|
||||
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 {
|
||||
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())
|
||||
if (self.focused_workspace()?.containers().len() == 1
|
||||
&& self.focused_workspace()?.floating_windows().is_empty())
|
||||
|| (self.focused_workspace()?.containers().is_empty()
|
||||
&& self.focused_workspace()?.floating_windows().len() == 1)
|
||||
{
|
||||
should_float = should_act(
|
||||
&title,
|
||||
&exe_name,
|
||||
&class,
|
||||
&path,
|
||||
&floating_applications,
|
||||
®ex_identifiers,
|
||||
)
|
||||
.is_some();
|
||||
// If after adding this window the workspace only contains 1 window, it
|
||||
// means it was previously empty and we focused the desktop to unfocus
|
||||
// any previous window from other workspace, so now we need to focus
|
||||
// this window again. This is needed because sometimes some windows
|
||||
// first send the `FocusChange` event and only the `Show` event after
|
||||
// and we will be focusing the desktop on the `FocusChange` event since
|
||||
// it is still empty.
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if workspace_contains_window {
|
||||
let mut monocle_window_event = false;
|
||||
if let Some(ref monocle) = monocle_container {
|
||||
if let Some(monocle_window) = monocle.focused_window() {
|
||||
if monocle_window.hwnd == window.hwnd {
|
||||
monocle_window_event = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if workspace_contains_window {
|
||||
let mut monocle_window_event = false;
|
||||
if let Some(ref monocle) = monocle_container {
|
||||
if let Some(monocle_window) = monocle.focused_window() {
|
||||
if monocle_window.hwnd == window.hwnd {
|
||||
monocle_window_event = true;
|
||||
}
|
||||
if !monocle_window_event && monocle_container.is_some() {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
if !monocle_window_event && monocle_container.is_some() {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::MoveResizeStart(_, window) => {
|
||||
if *self.focused_workspace()?.tile() {
|
||||
let monitor_idx = self.focused_monitor_idx();
|
||||
let workspace_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace_idx();
|
||||
let container_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace()
|
||||
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
|
||||
.focused_container_idx();
|
||||
let monitor_idx = self.focused_monitor_idx();
|
||||
let workspace_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace_idx();
|
||||
|
||||
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));
|
||||
}
|
||||
let pending_move_op = Arc::make_mut(&mut self.pending_move_op);
|
||||
*pending_move_op = Option::from((monitor_idx, workspace_idx, window.hwnd));
|
||||
}
|
||||
WindowManagerEvent::MoveResizeEnd(_, window) => {
|
||||
// We need this because if the event ends on a different monitor,
|
||||
// that monitor will already have been focused and updated in the state
|
||||
let pending = self.pending_move_op;
|
||||
let pending = *self.pending_move_op;
|
||||
// Always consume the pending move op whenever this event is handled
|
||||
self.pending_move_op = None;
|
||||
let pending_move_op = Arc::make_mut(&mut self.pending_move_op);
|
||||
*pending_move_op = None;
|
||||
|
||||
// If the window handles don't match then something went wrong and the pending move
|
||||
// is not related to this current move, if so abort this operation.
|
||||
if let Some((_, _, w_hwnd)) = pending {
|
||||
if w_hwnd != window.hwnd {
|
||||
color_eyre::eyre::bail!(
|
||||
"window handles for move operation don't match: {} != {}",
|
||||
w_hwnd,
|
||||
window.hwnd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let target_monitor_idx = self
|
||||
.monitor_idx_from_current_pos()
|
||||
@@ -454,8 +486,21 @@ impl WindowManager {
|
||||
// place across a monitor boundary to an empty workspace
|
||||
.unwrap_or(&Rect::default());
|
||||
|
||||
// This will be true if we have moved to an empty workspace on another monitor
|
||||
let mut moved_across_monitors = old_position == Rect::default();
|
||||
// This will be true if we have moved to another monitor
|
||||
let mut moved_across_monitors = false;
|
||||
|
||||
for (i, monitors) in self.monitors().iter().enumerate() {
|
||||
for workspace in monitors.workspaces() {
|
||||
if workspace.contains_window(window.hwnd) && i != target_monitor_idx {
|
||||
moved_across_monitors = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if moved_across_monitors {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((origin_monitor_idx, origin_workspace_idx, _)) = pending {
|
||||
// If we didn't move to another monitor with an empty workspace, it is
|
||||
// still possible that we moved to another monitor with a populated workspace
|
||||
@@ -476,8 +521,7 @@ impl WindowManager {
|
||||
.get(origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
|
||||
|
||||
let managed_window =
|
||||
origin_workspace.contains_managed_window(window.hwnd);
|
||||
let managed_window = origin_workspace.contains_window(window.hwnd);
|
||||
|
||||
if !managed_window {
|
||||
moved_across_monitors = false;
|
||||
@@ -487,7 +531,9 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if workspace.contains_managed_window(window.hwnd) || moved_across_monitors {
|
||||
if (*workspace.tile() && workspace.contains_managed_window(window.hwnd))
|
||||
|| moved_across_monitors
|
||||
{
|
||||
let resize = Rect {
|
||||
left: new_position.left - old_position.left,
|
||||
top: new_position.top - old_position.top,
|
||||
@@ -507,11 +553,8 @@ impl WindowManager {
|
||||
tracing::info!("moving with mouse");
|
||||
|
||||
if moved_across_monitors {
|
||||
if let Some((
|
||||
origin_monitor_idx,
|
||||
origin_workspace_idx,
|
||||
origin_container_idx,
|
||||
)) = pending
|
||||
if let Some((origin_monitor_idx, origin_workspace_idx, w_hwnd)) =
|
||||
pending
|
||||
{
|
||||
let target_workspace_idx = self
|
||||
.monitors()
|
||||
@@ -531,32 +574,36 @@ impl WindowManager {
|
||||
// Default to 0 in the case of an empty workspace
|
||||
.unwrap_or(0);
|
||||
|
||||
self.transfer_container(
|
||||
(
|
||||
origin_monitor_idx,
|
||||
origin_workspace_idx,
|
||||
origin_container_idx,
|
||||
),
|
||||
(
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
target_container_idx,
|
||||
),
|
||||
)?;
|
||||
let origin = (origin_monitor_idx, origin_workspace_idx, w_hwnd);
|
||||
let target = (
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
target_container_idx,
|
||||
);
|
||||
self.transfer_window(origin, target)?;
|
||||
|
||||
// We want to make sure both the origin and target monitors are updated,
|
||||
// so that we don't have ghost tiles until we force an interaction on
|
||||
// the origin monitor's focused workspace
|
||||
self.focus_monitor(origin_monitor_idx)?;
|
||||
self.focus_workspace(origin_workspace_idx)?;
|
||||
let origin_monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
|
||||
origin_monitor.focus_workspace(origin_workspace_idx)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
|
||||
self.focus_monitor(target_monitor_idx)?;
|
||||
self.focus_workspace(target_workspace_idx)?;
|
||||
let target_monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
|
||||
target_monitor.focus_workspace(target_workspace_idx)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
|
||||
// Make sure to give focus to the moved window again
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
// 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)?;
|
||||
@@ -663,6 +710,20 @@ impl WindowManager {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
for window in workspace.floating_windows() {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
|
||||
if let Some(window) = workspace.maximized_window() {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
|
||||
if let Some(container) = workspace.monocle_container() {
|
||||
for window in container.windows() {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
use crate::animation::PerAnimationPrefixConfig;
|
||||
use crate::animation::ANIMATION_DURATION_GLOBAL;
|
||||
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_FPS;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
|
||||
use crate::animation::DEFAULT_ANIMATION_FPS;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::ZOrder;
|
||||
use crate::border_manager::IMPLEMENTATION;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::colour::Colour;
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::StackbarLabel;
|
||||
@@ -20,6 +28,7 @@ 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::theme_manager;
|
||||
use crate::transparency_manager;
|
||||
use crate::window;
|
||||
use crate::window_manager::WindowManager;
|
||||
@@ -27,10 +36,6 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::CrossBoundaryBehaviour;
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_ENABLED;
|
||||
use crate::ANIMATION_FPS;
|
||||
use crate::ANIMATION_STYLE;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
@@ -41,6 +46,7 @@ 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::SLOW_APPLICATION_COMPENSATION_TIME;
|
||||
@@ -232,7 +238,7 @@ impl From<&Monitor> for MonitorConfig {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.30`
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.32`
|
||||
pub struct StaticConfig {
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -262,13 +268,13 @@ pub struct StaticConfig {
|
||||
/// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unmanaged_window_operation_behaviour: Option<OperationBehaviour>,
|
||||
/// END OF LIFE FEATURE: Determine focus follows mouse implementation (default: None)
|
||||
/// END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
/// Enable or disable mouse follows focus (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mouse_follows_focus: Option<bool>,
|
||||
/// Path to applications.yaml from komorebi-application-specific-configurations (default: None)
|
||||
/// Path to applications.json from komorebi-application-specific-configurations (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub app_specific_configuration_path: Option<PathBuf>,
|
||||
/// Width of the window border (default: 8)
|
||||
@@ -291,7 +297,7 @@ pub struct StaticConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(alias = "active_window_border_style")]
|
||||
pub border_style: Option<BorderStyle>,
|
||||
/// Active window border z-order (default: System)
|
||||
/// DEPRECATED from v0.1.31: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_z_order: Option<ZOrder>,
|
||||
/// Active window border implementation (default: Komorebi)
|
||||
@@ -368,16 +374,19 @@ pub struct StaticConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
// this option is a little special because it is only consumed by komorebic
|
||||
pub bar_configurations: Option<Vec<PathBuf>>,
|
||||
/// HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub remove_titlebar_applications: Option<Vec<MatchingRule>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AnimationsConfig {
|
||||
/// Enable or disable animations (default: false)
|
||||
enabled: bool,
|
||||
enabled: PerAnimationPrefixConfig<bool>,
|
||||
/// Set the animation duration in ms (default: 250)
|
||||
duration: Option<u64>,
|
||||
duration: Option<PerAnimationPrefixConfig<u64>>,
|
||||
/// Set the animation style (default: Linear)
|
||||
style: Option<AnimationStyle>,
|
||||
style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
|
||||
/// Set the animation FPS (default: 60)
|
||||
fps: Option<u64>,
|
||||
}
|
||||
@@ -386,7 +395,7 @@ pub struct AnimationsConfig {
|
||||
pub enum KomorebiTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
/// Name of the Catppuccin theme
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
/// Border colour when the container contains a single window (default: Blue)
|
||||
single_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
@@ -409,7 +418,7 @@ pub enum KomorebiTheme {
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
|
||||
name: komorebi_themes::Base16,
|
||||
/// Border colour when the container contains a single window (default: Base0D)
|
||||
single_border: Option<komorebi_themes::Base16Value>,
|
||||
@@ -457,6 +466,7 @@ impl StaticConfig {
|
||||
println!("\nEnd-of-life features will not receive any further bug fixes or updates; they should not be used\n")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aliases(raw: &str) {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("border", ["active_window_border"]);
|
||||
@@ -464,6 +474,8 @@ impl StaticConfig {
|
||||
map.insert("border_offset", ["active_window_border_offset"]);
|
||||
map.insert("border_colours", ["active_window_border_colours"]);
|
||||
map.insert("border_style", ["active_window_border_style"]);
|
||||
map.insert("applications.json", ["applications.yaml"]);
|
||||
map.insert("ignore_rules", ["float_rules"]);
|
||||
|
||||
let mut display = false;
|
||||
|
||||
@@ -488,7 +500,7 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
pub fn deprecated(raw: &str) {
|
||||
let deprecated_options = ["invisible_borders"];
|
||||
let deprecated_options = ["invisible_borders", "border_z_order"];
|
||||
let deprecated_variants = vec![
|
||||
("Hide", "window_hiding_behaviour", "Cloak"),
|
||||
("Minimize", "window_hiding_behaviour", "Cloak"),
|
||||
@@ -591,7 +603,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
),
|
||||
transparency_ignore_rules: None,
|
||||
border_style: Option::from(STYLE.load()),
|
||||
border_z_order: Option::from(Z_ORDER.load()),
|
||||
border_z_order: None,
|
||||
border_implementation: Option::from(IMPLEMENTATION.load()),
|
||||
default_workspace_padding: Option::from(
|
||||
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
|
||||
@@ -619,6 +631,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
),
|
||||
slow_application_identifiers: Option::from(SLOW_APPLICATION_IDENTIFIERS.lock().clone()),
|
||||
bar_configurations: None,
|
||||
remove_titlebar_applications: Option::from(NO_TITLEBAR.lock().clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -650,11 +663,43 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
if let Some(animations) = &self.animation {
|
||||
ANIMATION_ENABLED.store(animations.enabled, Ordering::SeqCst);
|
||||
ANIMATION_DURATION.store(animations.duration.unwrap_or(250), Ordering::SeqCst);
|
||||
ANIMATION_FPS.store(animations.fps.unwrap_or(60), Ordering::SeqCst);
|
||||
let mut animation_style = ANIMATION_STYLE.lock();
|
||||
*animation_style = animations.style.unwrap_or(AnimationStyle::Linear);
|
||||
match &animations.enabled {
|
||||
PerAnimationPrefixConfig::Prefix(enabled) => {
|
||||
ANIMATION_ENABLED_PER_ANIMATION.lock().clone_from(enabled);
|
||||
}
|
||||
PerAnimationPrefixConfig::Global(enabled) => {
|
||||
ANIMATION_ENABLED_GLOBAL.store(*enabled, Ordering::SeqCst);
|
||||
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
}
|
||||
|
||||
match &animations.style {
|
||||
Some(PerAnimationPrefixConfig::Prefix(style)) => {
|
||||
ANIMATION_STYLE_PER_ANIMATION.lock().clone_from(style);
|
||||
}
|
||||
Some(PerAnimationPrefixConfig::Global(style)) => {
|
||||
let mut animation_style = ANIMATION_STYLE_GLOBAL.lock();
|
||||
*animation_style = *style;
|
||||
ANIMATION_STYLE_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
match &animations.duration {
|
||||
Some(PerAnimationPrefixConfig::Prefix(duration)) => {
|
||||
ANIMATION_DURATION_PER_ANIMATION.lock().clone_from(duration);
|
||||
}
|
||||
Some(PerAnimationPrefixConfig::Global(duration)) => {
|
||||
ANIMATION_DURATION_GLOBAL.store(*duration, Ordering::SeqCst);
|
||||
ANIMATION_DURATION_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
ANIMATION_FPS.store(
|
||||
animations.fps.unwrap_or(DEFAULT_ANIMATION_FPS),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(container) = self.default_container_padding {
|
||||
@@ -733,6 +778,7 @@ impl StaticConfig {
|
||||
let mut transparency_blacklist = TRANSPARENCY_BLACKLIST.lock();
|
||||
let mut slow_application_identifiers = SLOW_APPLICATION_IDENTIFIERS.lock();
|
||||
let mut floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
let mut no_titlebar_applications = NO_TITLEBAR.lock();
|
||||
|
||||
if let Some(rules) = &mut self.ignore_rules {
|
||||
populate_rules(rules, &mut ignore_identifiers, &mut regex_identifiers)?;
|
||||
@@ -778,6 +824,10 @@ impl StaticConfig {
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut self.remove_titlebar_applications {
|
||||
populate_rules(rules, &mut no_titlebar_applications, &mut regex_identifiers)?;
|
||||
}
|
||||
|
||||
if let Some(stackbar) = &self.stackbar {
|
||||
if let Some(height) = &stackbar.height {
|
||||
STACKBAR_TAB_HEIGHT.store(*height, Ordering::SeqCst);
|
||||
@@ -815,151 +865,7 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
if let Some(theme) = &self.theme {
|
||||
let (
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
) = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name,
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Blue)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stack_border = stack_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let monocle_border = monocle_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Pink)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let floating_border = floating_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Yellow)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let unfocused_border = unfocused_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_unfocused_text = stackbar_unfocused_text
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Text)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_background = stackbar_background
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
|
||||
.color32(name.as_theme());
|
||||
|
||||
(
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
KomorebiTheme::Base16 {
|
||||
name,
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0D)
|
||||
.color32(*name);
|
||||
|
||||
let stack_border = stack_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(*name);
|
||||
|
||||
let monocle_border = monocle_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0F)
|
||||
.color32(*name);
|
||||
|
||||
let unfocused_border = unfocused_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(*name);
|
||||
|
||||
let floating_border = floating_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base09)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_unfocused_text = stackbar_unfocused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base05)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_background = stackbar_background
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(*name);
|
||||
|
||||
(
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
border_manager::FOCUSED.store(u32::from(Colour::from(single_border)), Ordering::SeqCst);
|
||||
border_manager::MONOCLE
|
||||
.store(u32::from(Colour::from(monocle_border)), Ordering::SeqCst);
|
||||
border_manager::STACK.store(u32::from(Colour::from(stack_border)), Ordering::SeqCst);
|
||||
border_manager::FLOATING
|
||||
.store(u32::from(Colour::from(floating_border)), Ordering::SeqCst);
|
||||
border_manager::UNFOCUSED
|
||||
.store(u32::from(Colour::from(unfocused_border)), Ordering::SeqCst);
|
||||
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_background)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
STACKBAR_FOCUSED_TEXT_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_focused_text)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_unfocused_text)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
theme_manager::send_notification(*theme);
|
||||
}
|
||||
|
||||
if let Some(path) = &self.app_specific_configuration_path {
|
||||
@@ -1192,8 +1098,9 @@ impl StaticConfig {
|
||||
mouse_follows_focus: value.mouse_follows_focus.unwrap_or(true),
|
||||
hotwatch: Hotwatch::new()?,
|
||||
has_pending_raise_op: false,
|
||||
pending_move_op: None,
|
||||
pending_move_op: Arc::new(None),
|
||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||
uncloack_to_ignore: 0,
|
||||
};
|
||||
|
||||
match value.focus_follows_mouse {
|
||||
@@ -1245,12 +1152,9 @@ impl StaticConfig {
|
||||
);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
ws.load_static_config(
|
||||
monitor
|
||||
.workspaces
|
||||
.get(j)
|
||||
.expect("no static workspace config"),
|
||||
)?;
|
||||
if let Some(workspace_config) = monitor.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1308,12 +1212,9 @@ impl StaticConfig {
|
||||
);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
ws.load_static_config(
|
||||
monitor
|
||||
.workspaces
|
||||
.get(j)
|
||||
.expect("no static workspace config"),
|
||||
)?;
|
||||
if let Some(workspace_config) = monitor.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
224
komorebi/src/theme_manager.rs
Normal file
224
komorebi/src/theme_manager.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::stackbar_manager;
|
||||
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::Colour;
|
||||
use crate::KomorebiTheme;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub struct Notification(KomorebiTheme);
|
||||
|
||||
pub static CURRENT_THEME: AtomicCell<Option<KomorebiTheme>> = AtomicCell::new(None);
|
||||
|
||||
impl Deref for Notification {
|
||||
type Target = KomorebiTheme;
|
||||
|
||||
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(theme: KomorebiTheme) {
|
||||
if event_tx().try_send(Notification(theme)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications() {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications() {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
|
||||
for notification in receiver {
|
||||
let theme = ¬ification.0;
|
||||
|
||||
let (
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
) = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name,
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Blue)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stack_border = stack_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let monocle_border = monocle_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Pink)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let floating_border = floating_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Yellow)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let unfocused_border = unfocused_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_unfocused_text = stackbar_unfocused_text
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Text)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_background = stackbar_background
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
|
||||
.color32(name.as_theme());
|
||||
|
||||
(
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
KomorebiTheme::Base16 {
|
||||
name,
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0D)
|
||||
.color32(*name);
|
||||
|
||||
let stack_border = stack_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(*name);
|
||||
|
||||
let monocle_border = monocle_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0F)
|
||||
.color32(*name);
|
||||
|
||||
let unfocused_border = unfocused_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(*name);
|
||||
|
||||
let floating_border = floating_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base09)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_unfocused_text = stackbar_unfocused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base05)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_background = stackbar_background
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(*name);
|
||||
|
||||
(
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
border_manager::FOCUSED.store(u32::from(Colour::from(single_border)), Ordering::SeqCst);
|
||||
border_manager::MONOCLE.store(u32::from(Colour::from(monocle_border)), Ordering::SeqCst);
|
||||
border_manager::STACK.store(u32::from(Colour::from(stack_border)), Ordering::SeqCst);
|
||||
border_manager::FLOATING.store(u32::from(Colour::from(floating_border)), Ordering::SeqCst);
|
||||
border_manager::UNFOCUSED
|
||||
.store(u32::from(Colour::from(unfocused_border)), Ordering::SeqCst);
|
||||
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_background)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
STACKBAR_FOCUSED_TEXT_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_focused_text)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_unfocused_text)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
CURRENT_THEME.store(Some(notification.0));
|
||||
|
||||
border_manager::send_notification(None);
|
||||
stackbar_manager::send_notification();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
use crate::border_manager;
|
||||
use crate::animation::lerp::Lerp;
|
||||
use crate::animation::prefix::new_animation_key;
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
use crate::animation::AnimationEngine;
|
||||
use crate::animation::RenderDispatcher;
|
||||
use crate::animation::ANIMATION_DURATION_GLOBAL;
|
||||
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_MANAGER;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
|
||||
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::AnimationStyle;
|
||||
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
|
||||
use crate::SLOW_APPLICATION_IDENTIFIERS;
|
||||
use std::collections::HashMap;
|
||||
@@ -15,6 +24,7 @@ use std::fmt::Formatter;
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::core::config_generation::IdWithIdentifier;
|
||||
@@ -35,12 +45,12 @@ 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::FLOATING_APPLICATIONS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
@@ -57,16 +67,11 @@ 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,
|
||||
animation: Animation::new(value),
|
||||
}
|
||||
Self { hwnd: value }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +79,6 @@ impl From<HWND> for Window {
|
||||
fn from(value: HWND) -> Self {
|
||||
Self {
|
||||
hwnd: value.0 as isize,
|
||||
animation: Animation::new(value.0 as isize),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,6 +158,144 @@ impl Serialize for Window {
|
||||
}
|
||||
}
|
||||
|
||||
struct MovementRenderDispatcher {
|
||||
hwnd: isize,
|
||||
start_rect: Rect,
|
||||
target_rect: Rect,
|
||||
top: bool,
|
||||
style: AnimationStyle,
|
||||
}
|
||||
|
||||
impl MovementRenderDispatcher {
|
||||
const PREFIX: AnimationPrefix = AnimationPrefix::Movement;
|
||||
|
||||
pub fn new(
|
||||
hwnd: isize,
|
||||
start_rect: Rect,
|
||||
target_rect: Rect,
|
||||
top: bool,
|
||||
style: AnimationStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
hwnd,
|
||||
start_rect,
|
||||
target_rect,
|
||||
top,
|
||||
style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderDispatcher for MovementRenderDispatcher {
|
||||
fn get_animation_key(&self) -> String {
|
||||
new_animation_key(MovementRenderDispatcher::PREFIX, self.hwnd.to_string())
|
||||
}
|
||||
|
||||
fn pre_render(&self) -> Result<()> {
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, progress: f64) -> Result<()> {
|
||||
let new_rect = self.start_rect.lerp(self.target_rect, progress, self.style);
|
||||
|
||||
// using MoveWindow because it runs faster than SetWindowPos
|
||||
// so animation have more fps and feel smoother
|
||||
WindowsApi::move_window(self.hwnd, &new_rect, false)?;
|
||||
WindowsApi::invalidate_rect(self.hwnd, None, false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_render(&self) -> Result<()> {
|
||||
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top)?;
|
||||
if ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX)
|
||||
== 0
|
||||
{
|
||||
if WindowsApi::foreground_window().unwrap_or_default() == self.hwnd {
|
||||
focus_manager::send_notification(self.hwnd)
|
||||
}
|
||||
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
|
||||
stackbar_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct TransparencyRenderDispatcher {
|
||||
hwnd: isize,
|
||||
start_opacity: u8,
|
||||
target_opacity: u8,
|
||||
style: AnimationStyle,
|
||||
is_opaque: bool,
|
||||
}
|
||||
|
||||
impl TransparencyRenderDispatcher {
|
||||
const PREFIX: AnimationPrefix = AnimationPrefix::Transparency;
|
||||
|
||||
pub fn new(
|
||||
hwnd: isize,
|
||||
is_opaque: bool,
|
||||
start_opacity: u8,
|
||||
target_opacity: u8,
|
||||
style: AnimationStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
hwnd,
|
||||
start_opacity,
|
||||
target_opacity,
|
||||
style,
|
||||
is_opaque,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderDispatcher for TransparencyRenderDispatcher {
|
||||
fn get_animation_key(&self) -> String {
|
||||
new_animation_key(TransparencyRenderDispatcher::PREFIX, self.hwnd.to_string())
|
||||
}
|
||||
|
||||
fn pre_render(&self) -> Result<()> {
|
||||
//transparent
|
||||
if !self.is_opaque {
|
||||
let window = Window::from(self.hwnd);
|
||||
let mut ex_style = window.ex_style()?;
|
||||
ex_style.insert(ExtendedWindowStyle::LAYERED);
|
||||
window.update_ex_style(&ex_style)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, progress: f64) -> Result<()> {
|
||||
WindowsApi::set_transparent(
|
||||
self.hwnd,
|
||||
self.start_opacity
|
||||
.lerp(self.target_opacity, progress, self.style),
|
||||
)
|
||||
}
|
||||
|
||||
fn post_render(&self) -> Result<()> {
|
||||
//opaque
|
||||
if self.is_opaque {
|
||||
let window = Window::from(self.hwnd);
|
||||
let mut ex_style = window.ex_style()?;
|
||||
ex_style.remove(ExtendedWindowStyle::LAYERED);
|
||||
window.update_ex_style(&ex_style)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub const fn hwnd(self) -> HWND {
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
@@ -171,17 +313,59 @@ impl Window {
|
||||
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 left = x_diff + window_x;
|
||||
let top = y_diff + window_y;
|
||||
|
||||
let corrected_width = (current_rect.right as f32 * x_ratio) as i32;
|
||||
let corrected_height = (current_rect.bottom as f32 * y_ratio) as i32;
|
||||
|
||||
let new_rect = Rect {
|
||||
left: x_diff + window_x,
|
||||
top: y_diff + window_y,
|
||||
right: current_rect.right,
|
||||
bottom: current_rect.bottom,
|
||||
left,
|
||||
top,
|
||||
right: corrected_width,
|
||||
bottom: corrected_height,
|
||||
};
|
||||
//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)
|
||||
let is_maximized = &new_rect == target_area;
|
||||
if is_maximized {
|
||||
windows_api::WindowsApi::unmaximize_window(self.hwnd);
|
||||
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
|
||||
let move_enabled = animation_enabled
|
||||
.get(&MovementRenderDispatcher::PREFIX)
|
||||
.is_some_and(|v| *v);
|
||||
drop(animation_enabled);
|
||||
|
||||
if move_enabled || ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst) {
|
||||
let anim_count = ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX);
|
||||
self.set_position(&new_rect, true)?;
|
||||
let hwnd = self.hwnd;
|
||||
// Wait for the animation to finish before maximizing the window again, otherwise
|
||||
// we would be maximizing the window on the current monitor anyway
|
||||
thread::spawn(move || {
|
||||
let mut new_anim_count = ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX);
|
||||
let mut max_wait = 2000; // Max waiting time. No one will be using an animation longer than 2s, right? RIGHT??? WHY?
|
||||
while new_anim_count > anim_count && max_wait > 0 {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
new_anim_count = ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX);
|
||||
max_wait -= 1;
|
||||
}
|
||||
windows_api::WindowsApi::maximize_window(hwnd);
|
||||
});
|
||||
} else {
|
||||
self.set_position(&new_rect, true)?;
|
||||
windows_api::WindowsApi::maximize_window(self.hwnd);
|
||||
}
|
||||
} else {
|
||||
self.set_position(&new_rect, true)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
|
||||
@@ -199,53 +383,6 @@ 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<()> {
|
||||
let window_rect = WindowsApi::window_rect(self.hwnd)?;
|
||||
|
||||
@@ -253,8 +390,27 @@ impl Window {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if ANIMATION_ENABLED.load(Ordering::SeqCst) {
|
||||
self.animate_position(&window_rect, layout, top)
|
||||
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
|
||||
let move_enabled = animation_enabled.get(&MovementRenderDispatcher::PREFIX);
|
||||
|
||||
if move_enabled.is_some_and(|enabled| *enabled)
|
||||
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
|
||||
{
|
||||
let duration = Duration::from_millis(
|
||||
*ANIMATION_DURATION_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&MovementRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
|
||||
);
|
||||
let style = *ANIMATION_STYLE_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&MovementRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
|
||||
|
||||
let render_dispatcher =
|
||||
MovementRenderDispatcher::new(self.hwnd, window_rect, *layout, top, style);
|
||||
|
||||
AnimationEngine::animate(render_dispatcher, duration)
|
||||
} else {
|
||||
WindowsApi::position_window(self.hwnd, layout, top)
|
||||
}
|
||||
@@ -362,20 +518,81 @@ impl Window {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_focused(self) -> bool {
|
||||
WindowsApi::foreground_window().unwrap_or_default() == self.hwnd
|
||||
}
|
||||
|
||||
pub fn transparent(self) -> Result<()> {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.insert(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)?;
|
||||
WindowsApi::set_transparent(
|
||||
self.hwnd,
|
||||
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
|
||||
)
|
||||
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
|
||||
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
|
||||
|
||||
if transparent_enabled.is_some_and(|enabled| *enabled)
|
||||
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
|
||||
{
|
||||
let duration = Duration::from_millis(
|
||||
*ANIMATION_DURATION_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&TransparencyRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
|
||||
);
|
||||
let style = *ANIMATION_STYLE_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&TransparencyRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
|
||||
|
||||
let render_dispatcher = TransparencyRenderDispatcher::new(
|
||||
self.hwnd,
|
||||
false,
|
||||
WindowsApi::get_transparent(self.hwnd).unwrap_or(255),
|
||||
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
|
||||
style,
|
||||
);
|
||||
|
||||
AnimationEngine::animate(render_dispatcher, duration)
|
||||
} else {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.insert(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)?;
|
||||
WindowsApi::set_transparent(
|
||||
self.hwnd,
|
||||
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn opaque(self) -> Result<()> {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.remove(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)
|
||||
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
|
||||
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
|
||||
|
||||
if transparent_enabled.is_some_and(|enabled| *enabled)
|
||||
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
|
||||
{
|
||||
let duration = Duration::from_millis(
|
||||
*ANIMATION_DURATION_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&TransparencyRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
|
||||
);
|
||||
let style = *ANIMATION_STYLE_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&TransparencyRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
|
||||
|
||||
let render_dispatcher = TransparencyRenderDispatcher::new(
|
||||
self.hwnd,
|
||||
true,
|
||||
WindowsApi::get_transparent(self.hwnd)
|
||||
.unwrap_or(transparency_manager::TRANSPARENCY_ALPHA.load_consume()),
|
||||
255,
|
||||
style,
|
||||
);
|
||||
|
||||
AnimationEngine::animate(render_dispatcher, duration)
|
||||
} else {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.remove(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_accent(self, colour: u32) -> Result<()> {
|
||||
@@ -565,8 +782,9 @@ pub struct RuleDebug {
|
||||
pub matches_ignore_identifier: Option<MatchingRule>,
|
||||
pub matches_managed_override: Option<MatchingRule>,
|
||||
pub matches_layered_whitelist: Option<MatchingRule>,
|
||||
pub matches_floating_applications: Option<MatchingRule>,
|
||||
pub matches_wsl2_gui: Option<String>,
|
||||
pub matches_no_titlebar: Option<String>,
|
||||
pub matches_no_titlebar: Option<MatchingRule>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -592,7 +810,7 @@ fn window_is_eligible(
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
let ignore_identifiers = IGNORE_IDENTIFIERS.lock();
|
||||
let should_float = if let Some(rule) = should_act(
|
||||
let should_ignore = if let Some(rule) = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
@@ -621,7 +839,19 @@ fn window_is_eligible(
|
||||
false
|
||||
};
|
||||
|
||||
if should_float && !managed_override {
|
||||
let floating_identifiers = FLOATING_APPLICATIONS.lock();
|
||||
if let Some(rule) = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&floating_identifiers,
|
||||
®ex_identifiers,
|
||||
) {
|
||||
debug.matches_floating_applications = Some(rule);
|
||||
}
|
||||
|
||||
if should_ignore && !managed_override {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -659,9 +889,19 @@ fn window_is_eligible(
|
||||
allow
|
||||
};
|
||||
|
||||
let allow_titlebar_removed = {
|
||||
let titlebars_removed = NO_TITLEBAR.lock();
|
||||
titlebars_removed.contains(exe_name)
|
||||
let titlebars_removed = NO_TITLEBAR.lock();
|
||||
let allow_titlebar_removed = if let Some(rule) = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&titlebars_removed,
|
||||
®ex_identifiers,
|
||||
) {
|
||||
debug.matches_no_titlebar = Some(rule);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
{
|
||||
@@ -763,10 +1003,7 @@ pub fn should_act_individual(
|
||||
let mut should_act = false;
|
||||
|
||||
match identifier.matching_strategy {
|
||||
None => {
|
||||
panic!("there is no matching strategy identified for this rule");
|
||||
}
|
||||
Some(MatchingStrategy::Legacy) => match identifier.kind {
|
||||
None | Some(MatchingStrategy::Legacy) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::env::temp_dir;
|
||||
use std::io::ErrorKind;
|
||||
use std::net::Shutdown;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -20,7 +22,11 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use crate::animation::AnimationEngine;
|
||||
use crate::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::custom_layout::CustomLayout;
|
||||
use crate::core::Arrangement;
|
||||
@@ -50,6 +56,7 @@ use crate::current_virtual_desktop;
|
||||
use crate::load_configuration;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
use crate::should_act;
|
||||
use crate::should_act_individual;
|
||||
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_LABEL;
|
||||
@@ -60,6 +67,8 @@ use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
|
||||
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::transparency_manager;
|
||||
use crate::transparency_manager::TRANSPARENCY_ALPHA;
|
||||
use crate::transparency_manager::TRANSPARENCY_ENABLED;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
@@ -82,6 +91,8 @@ use crate::NO_TITLEBAR;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::SUBSCRIPTION_SOCKETS;
|
||||
use crate::TRANSPARENCY_BLACKLIST;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
|
||||
@@ -102,8 +113,9 @@ pub struct WindowManager {
|
||||
pub hotwatch: Hotwatch,
|
||||
pub virtual_desktop_id: Option<Vec<u8>>,
|
||||
pub has_pending_raise_op: bool,
|
||||
pub pending_move_op: Option<(usize, usize, usize)>,
|
||||
pub pending_move_op: Arc<Option<(usize, usize, isize)>>,
|
||||
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
|
||||
pub uncloack_to_ignore: usize,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
@@ -185,6 +197,9 @@ pub struct GlobalState {
|
||||
pub stackbar_tab_background_colour: Colour,
|
||||
pub stackbar_tab_width: i32,
|
||||
pub stackbar_height: i32,
|
||||
pub transparency_enabled: bool,
|
||||
pub transparency_alpha: u8,
|
||||
pub transparency_blacklist: Vec<MatchingRule>,
|
||||
pub remove_titlebars: bool,
|
||||
#[serde(alias = "float_identifiers")]
|
||||
pub ignore_identifiers: Vec<MatchingRule>,
|
||||
@@ -238,6 +253,9 @@ impl Default for GlobalState {
|
||||
)),
|
||||
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
|
||||
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
|
||||
transparency_enabled: TRANSPARENCY_ENABLED.load(Ordering::SeqCst),
|
||||
transparency_alpha: TRANSPARENCY_ALPHA.load(Ordering::SeqCst),
|
||||
transparency_blacklist: TRANSPARENCY_BLACKLIST.lock().clone(),
|
||||
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
|
||||
ignore_identifiers: IGNORE_IDENTIFIERS.lock().clone(),
|
||||
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
|
||||
@@ -288,6 +306,7 @@ struct EnforceWorkspaceRuleOp {
|
||||
origin_workspace_idx: usize,
|
||||
target_monitor_idx: usize,
|
||||
target_workspace_idx: usize,
|
||||
floating: bool,
|
||||
}
|
||||
impl EnforceWorkspaceRuleOp {
|
||||
const fn is_origin(&self, monitor_idx: usize, workspace_idx: usize) -> bool {
|
||||
@@ -338,8 +357,9 @@ impl WindowManager {
|
||||
mouse_follows_focus: true,
|
||||
hotwatch: Hotwatch::new()?,
|
||||
has_pending_raise_op: false,
|
||||
pending_move_op: None,
|
||||
pending_move_op: Arc::new(None),
|
||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||
uncloack_to_ignore: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -350,6 +370,140 @@ impl WindowManager {
|
||||
WindowsApi::load_workspace_information(&mut self.monitors)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, state))]
|
||||
pub fn apply_state(&mut self, state: State) {
|
||||
let mut can_apply = true;
|
||||
|
||||
let state_monitors_len = state.monitors.elements().len();
|
||||
let current_monitors_len = self.monitors.elements().len();
|
||||
if state_monitors_len != current_monitors_len {
|
||||
tracing::warn!(
|
||||
"cannot apply state from {}; state file has {state_monitors_len} monitors, but only {current_monitors_len} are currently connected",
|
||||
temp_dir().join("komorebi.state.json").to_string_lossy()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for monitor in state.monitors.elements() {
|
||||
for workspace in monitor.workspaces() {
|
||||
for container in workspace.containers() {
|
||||
for window in container.windows() {
|
||||
if window.exe().is_err() {
|
||||
can_apply = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = workspace.maximized_window() {
|
||||
if window.exe().is_err() {
|
||||
can_apply = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(container) = workspace.monocle_container() {
|
||||
for window in container.windows() {
|
||||
if window.exe().is_err() {
|
||||
can_apply = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for window in workspace.floating_windows() {
|
||||
if window.exe().is_err() {
|
||||
can_apply = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if can_apply {
|
||||
tracing::info!(
|
||||
"applying state from {}",
|
||||
temp_dir().join("komorebi.state.json").to_string_lossy()
|
||||
);
|
||||
|
||||
let offset = self.work_area_offset;
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() {
|
||||
let mut focused_workspace = 0;
|
||||
for (workspace_idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx) {
|
||||
if let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
|
||||
{
|
||||
*workspace = state_workspace.clone();
|
||||
if state_monitor.focused_workspace_idx() == workspace_idx {
|
||||
focused_workspace = workspace_idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(error) = monitor.focus_workspace(focused_workspace) {
|
||||
tracing::warn!(
|
||||
"cannot focus workspace '{focused_workspace}' on monitor '{monitor_idx}' from {}: {}",
|
||||
temp_dir().join("komorebi.state.json").to_string_lossy(),
|
||||
error,
|
||||
);
|
||||
}
|
||||
if let Err(error) = monitor.load_focused_workspace(mouse_follows_focus) {
|
||||
tracing::warn!(
|
||||
"cannot load focused workspace '{focused_workspace}' on monitor '{monitor_idx}' from {}: {}",
|
||||
temp_dir().join("komorebi.state.json").to_string_lossy(),
|
||||
error,
|
||||
);
|
||||
}
|
||||
if let Err(error) = monitor.update_focused_workspace(offset) {
|
||||
tracing::warn!(
|
||||
"cannot update workspace '{focused_workspace}' on monitor '{monitor_idx}' from {}: {}",
|
||||
temp_dir().join("komorebi.state.json").to_string_lossy(),
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let focused_monitor_idx = state.monitors.focused_idx();
|
||||
let focused_workspace_idx = state
|
||||
.monitors
|
||||
.elements()
|
||||
.get(focused_monitor_idx)
|
||||
.map(|m| m.focused_workspace_idx())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Err(error) = self.focus_monitor(focused_monitor_idx) {
|
||||
tracing::warn!(
|
||||
"cannot focus monitor '{focused_monitor_idx}' from {}: {}",
|
||||
temp_dir().join("komorebi.state.json").to_string_lossy(),
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(error) = self.focus_workspace(focused_workspace_idx) {
|
||||
tracing::warn!(
|
||||
"cannot focus workspace '{focused_workspace_idx}' on monitor '{focused_monitor_idx}' from {}: {}",
|
||||
temp_dir().join("komorebi.state.json").to_string_lossy(),
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(error) = self.update_focused_workspace(true, true) {
|
||||
tracing::warn!(
|
||||
"cannot update focused workspace '{focused_workspace_idx}' on monitor '{focused_monitor_idx}' from {}: {}",
|
||||
temp_dir().join("komorebi.state.json").to_string_lossy(),
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"cannot apply state from {}; some windows referenced in the state file no longer exist",
|
||||
temp_dir().join("komorebi.state.json").to_string_lossy()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn reload_configuration() {
|
||||
tracing::info!("reloading configuration");
|
||||
@@ -495,6 +649,35 @@ impl WindowManager {
|
||||
None
|
||||
}
|
||||
|
||||
/// Calculates the direction of a move across monitors given a specific monitor index
|
||||
pub fn direction_from_monitor_idx(
|
||||
&self,
|
||||
target_monitor_idx: usize,
|
||||
) -> Option<OperationDirection> {
|
||||
let current_monitor_idx = self.focused_monitor_idx();
|
||||
if current_monitor_idx == target_monitor_idx {
|
||||
return None;
|
||||
}
|
||||
|
||||
let current_monitor_size = self.focused_monitor_size().ok()?;
|
||||
let target_monitor_size = *self.monitors().get(target_monitor_idx)?.size();
|
||||
|
||||
if target_monitor_size.left + target_monitor_size.right == current_monitor_size.left {
|
||||
return Some(OperationDirection::Left);
|
||||
}
|
||||
if current_monitor_size.right + current_monitor_size.left == target_monitor_size.left {
|
||||
return Some(OperationDirection::Right);
|
||||
}
|
||||
if target_monitor_size.top + target_monitor_size.bottom == current_monitor_size.top {
|
||||
return Some(OperationDirection::Up);
|
||||
}
|
||||
if current_monitor_size.top + current_monitor_size.bottom == target_monitor_size.top {
|
||||
return Some(OperationDirection::Down);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
fn add_window_handle_to_move_based_on_workspace_rule(
|
||||
@@ -505,6 +688,7 @@ impl WindowManager {
|
||||
origin_workspace_idx: usize,
|
||||
target_monitor_idx: usize,
|
||||
target_workspace_idx: usize,
|
||||
floating: bool,
|
||||
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
|
||||
) -> () {
|
||||
tracing::trace!(
|
||||
@@ -521,6 +705,7 @@ impl WindowManager {
|
||||
origin_workspace_idx,
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
floating,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -576,6 +761,8 @@ impl WindowManager {
|
||||
};
|
||||
|
||||
if matched {
|
||||
let floating = workspace.floating_windows().contains(window);
|
||||
|
||||
if rule.initial_only {
|
||||
if !already_moved_window_handles.contains(&window.hwnd) {
|
||||
already_moved_window_handles.insert(window.hwnd);
|
||||
@@ -587,6 +774,7 @@ impl WindowManager {
|
||||
j,
|
||||
rule.monitor_index,
|
||||
rule.workspace_index,
|
||||
floating,
|
||||
&mut to_move,
|
||||
);
|
||||
}
|
||||
@@ -598,6 +786,7 @@ impl WindowManager {
|
||||
j,
|
||||
rule.monitor_index,
|
||||
rule.workspace_index,
|
||||
floating,
|
||||
&mut to_move,
|
||||
);
|
||||
}
|
||||
@@ -616,17 +805,34 @@ impl WindowManager {
|
||||
|
||||
// Parse the operation and remove any windows that are not placed according to their rules
|
||||
for op in &to_move {
|
||||
let origin_workspace = self
|
||||
let target_area = *self
|
||||
.monitors_mut()
|
||||
.get_mut(op.target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
|
||||
.work_area_size();
|
||||
|
||||
let origin_monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(op.origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
|
||||
.ok_or_else(|| anyhow!("there is no monitor with that index"))?;
|
||||
|
||||
let origin_area = *origin_monitor.work_area_size();
|
||||
|
||||
let origin_workspace = origin_monitor
|
||||
.workspaces_mut()
|
||||
.get_mut(op.origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace with that index"))?;
|
||||
|
||||
let mut window = Window::from(op.hwnd);
|
||||
|
||||
// If it is a floating window move it to the target area
|
||||
if op.floating {
|
||||
window.move_to_area(&origin_area, &target_area)?;
|
||||
}
|
||||
|
||||
// Hide the window we are about to remove if it is on the currently focused workspace
|
||||
if op.is_origin(focused_monitor_idx, focused_workspace_idx) {
|
||||
Window::from(op.hwnd).hide();
|
||||
window.hide();
|
||||
should_update_focused_workspace = true;
|
||||
}
|
||||
|
||||
@@ -656,7 +862,21 @@ impl WindowManager {
|
||||
.get_mut(op.target_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace with that index"))?;
|
||||
|
||||
target_workspace.new_container_for_window(Window::from(op.hwnd));
|
||||
if op.floating {
|
||||
target_workspace
|
||||
.floating_windows_mut()
|
||||
.push(Window::from(op.hwnd));
|
||||
} else {
|
||||
//TODO(alex-ds13): should this take into account the target workspace
|
||||
//`window_container_behaviour`?
|
||||
//In the case above a floating window should always be moved as floating,
|
||||
//because it was set as so either manually by the user or by a
|
||||
//`floating_applications` rule so it should stay that way. But a tiled window
|
||||
//when moving to another workspace by a `workspace_rule` should honor that
|
||||
//workspace `window_container_behaviour` in my opinion! Maybe this should be done
|
||||
//on the `new_container_for_window` function instead.
|
||||
target_workspace.new_container_for_window(Window::from(op.hwnd));
|
||||
}
|
||||
}
|
||||
|
||||
// Only re-tile the focused workspace if we need to
|
||||
@@ -781,6 +1001,133 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn transfer_window(
|
||||
&mut self,
|
||||
origin: (usize, usize, isize),
|
||||
target: (usize, usize, usize),
|
||||
) -> Result<()> {
|
||||
let (origin_monitor_idx, origin_workspace_idx, w_hwnd) = origin;
|
||||
let (target_monitor_idx, target_workspace_idx, target_container_idx) = target;
|
||||
|
||||
let origin_workspace = self
|
||||
.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("cannot get monitor idx"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
|
||||
|
||||
let origin_container_idx = origin_workspace
|
||||
.container_for_window(w_hwnd)
|
||||
.and_then(|c| origin_workspace.containers().iter().position(|cc| cc == c));
|
||||
|
||||
if let Some(origin_container_idx) = origin_container_idx {
|
||||
// Moving normal container window
|
||||
self.transfer_container(
|
||||
(
|
||||
origin_monitor_idx,
|
||||
origin_workspace_idx,
|
||||
origin_container_idx,
|
||||
),
|
||||
(
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
target_container_idx,
|
||||
),
|
||||
)?;
|
||||
} else if let Some(idx) = origin_workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.position(|w| w.hwnd == w_hwnd)
|
||||
{
|
||||
// Moving floating window
|
||||
// There is no need to physically move the floating window between areas with
|
||||
// `move_to_area` because the user already did that, so we only need to transfer the
|
||||
// window to the target `floating_windows`
|
||||
let floating_window = origin_workspace.floating_windows_mut().remove(idx);
|
||||
|
||||
let target_workspace = self
|
||||
.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused workspace for this monitor"))?;
|
||||
|
||||
target_workspace
|
||||
.floating_windows_mut()
|
||||
.push(floating_window);
|
||||
} else if origin_workspace
|
||||
.monocle_container()
|
||||
.as_ref()
|
||||
.and_then(|monocle| monocle.focused_window().map(|w| w.hwnd == w_hwnd))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// Moving monocle container
|
||||
if let Some(monocle_idx) = origin_workspace.monocle_container_restore_idx() {
|
||||
let origin_workspace = self
|
||||
.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace for this monitor"))?;
|
||||
let mut uncloack_amount = 0;
|
||||
for container in origin_workspace.containers_mut() {
|
||||
container.restore();
|
||||
uncloack_amount += 1;
|
||||
}
|
||||
origin_workspace.reintegrate_monocle_container()?;
|
||||
|
||||
self.transfer_container(
|
||||
(origin_monitor_idx, origin_workspace_idx, monocle_idx),
|
||||
(
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
target_container_idx,
|
||||
),
|
||||
)?;
|
||||
// After we restore the origin workspace, some windows that were cloacked
|
||||
// by the monocle might now be uncloacked which would trigger a workspace
|
||||
// reconciliation since the focused monitor would be different from origin.
|
||||
// That workspace reconciliation would focus the window on the origin monitor.
|
||||
// So we need to ignore the uncloak events produced by the origin workspace
|
||||
// restore to avoid that issue.
|
||||
self.uncloack_to_ignore = uncloack_amount;
|
||||
}
|
||||
} else if origin_workspace
|
||||
.maximized_window()
|
||||
.as_ref()
|
||||
.map(|max| max.hwnd == w_hwnd)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// Moving maximized_window
|
||||
if let Some(maximized_idx) = origin_workspace.maximized_window_restore_idx() {
|
||||
self.focus_monitor(origin_monitor_idx)?;
|
||||
let origin_monitor = self
|
||||
.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no origin monitor"))?;
|
||||
origin_monitor.focus_workspace(origin_workspace_idx)?;
|
||||
self.unmaximize_window()?;
|
||||
self.focus_monitor(target_monitor_idx)?;
|
||||
let target_monitor = self
|
||||
.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no target monitor"))?;
|
||||
target_monitor.focus_workspace(target_workspace_idx)?;
|
||||
|
||||
self.transfer_container(
|
||||
(origin_monitor_idx, origin_workspace_idx, maximized_idx),
|
||||
(
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
target_container_idx,
|
||||
),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn transfer_container(
|
||||
&mut self,
|
||||
@@ -1046,10 +1393,45 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn restore_all_windows(&mut self) -> Result<()> {
|
||||
pub fn stop(&mut self, ignore_restore: bool) -> Result<()> {
|
||||
tracing::info!(
|
||||
"received stop command, restoring all hidden windows and terminating process"
|
||||
);
|
||||
|
||||
let state = &State::from(&*self);
|
||||
std::fs::write(
|
||||
temp_dir().join("komorebi.state.json"),
|
||||
serde_json::to_string_pretty(&state)?,
|
||||
)?;
|
||||
|
||||
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
|
||||
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
|
||||
self.restore_all_windows(ignore_restore)?;
|
||||
AnimationEngine::wait_for_all_animations();
|
||||
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
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)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn restore_all_windows(&mut self, ignore_restore: bool) -> Result<()> {
|
||||
tracing::info!("restoring all hidden windows");
|
||||
|
||||
let no_titlebar = NO_TITLEBAR.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
let known_transparent_hwnds = transparency_manager::known_hwnds();
|
||||
let border_implementation = border_manager::IMPLEMENTATION.load();
|
||||
|
||||
@@ -1065,7 +1447,17 @@ impl WindowManager {
|
||||
|
||||
for containers in workspace.containers_mut() {
|
||||
for window in containers.windows_mut() {
|
||||
if no_titlebar.contains(&window.exe()?) {
|
||||
let should_remove_titlebar_for_window = should_act(
|
||||
&window.title().unwrap_or_default(),
|
||||
&window.exe().unwrap_or_default(),
|
||||
&window.class().unwrap_or_default(),
|
||||
&window.path().unwrap_or_default(),
|
||||
&no_titlebar,
|
||||
®ex_identifiers,
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if should_remove_titlebar_for_window {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
@@ -1077,7 +1469,9 @@ impl WindowManager {
|
||||
window.remove_accent()?;
|
||||
}
|
||||
|
||||
window.restore();
|
||||
if !ignore_restore {
|
||||
window.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1216,6 +1610,7 @@ impl WindowManager {
|
||||
monitor_idx: usize,
|
||||
workspace_idx: Option<usize>,
|
||||
follow: bool,
|
||||
move_direction: Option<OperationDirection>,
|
||||
) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
@@ -1270,8 +1665,12 @@ impl WindowManager {
|
||||
.get_mut(monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
let mut should_load_workspace = false;
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
target_monitor.focus_workspace(workspace_idx)?;
|
||||
if workspace_idx != target_monitor.focused_workspace_idx() {
|
||||
target_monitor.focus_workspace(workspace_idx)?;
|
||||
should_load_workspace = true;
|
||||
}
|
||||
}
|
||||
let target_workspace = target_monitor
|
||||
.focused_workspace_mut()
|
||||
@@ -1288,7 +1687,11 @@ impl WindowManager {
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
target_monitor.add_container(container, workspace_idx)?;
|
||||
if let Some(direction) = move_direction {
|
||||
target_monitor.add_container_with_direction(container, workspace_idx, direction)?;
|
||||
} else {
|
||||
target_monitor.add_container(container, workspace_idx)?;
|
||||
}
|
||||
|
||||
if let Some(workspace) = target_monitor.focused_workspace() {
|
||||
if !*workspace.tile() {
|
||||
@@ -1302,7 +1705,9 @@ impl WindowManager {
|
||||
bail!("failed to find a window to move");
|
||||
}
|
||||
|
||||
target_monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
if should_load_workspace {
|
||||
target_monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
}
|
||||
target_monitor.update_focused_workspace(offset)?;
|
||||
|
||||
// this second one is for DPI changes when the target is another monitor
|
||||
@@ -1381,13 +1786,14 @@ impl WindowManager {
|
||||
|
||||
tracing::info!("focusing container");
|
||||
|
||||
let new_idx = if workspace.monocle_container().is_some() {
|
||||
None
|
||||
} else {
|
||||
workspace.new_idx_for_direction(direction)
|
||||
};
|
||||
let new_idx =
|
||||
if workspace.maximized_window().is_some() || workspace.monocle_container().is_some() {
|
||||
None
|
||||
} else {
|
||||
workspace.new_idx_for_direction(direction)
|
||||
};
|
||||
|
||||
let mut cross_monitor_monocle = false;
|
||||
let mut cross_monitor_monocle_or_max = false;
|
||||
|
||||
// this is for when we are scrolling across workspaces like PaperWM
|
||||
if new_idx.is_none()
|
||||
@@ -1464,14 +1870,24 @@ impl WindowManager {
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
|
||||
if let Ok(focused_workspace) = self.focused_workspace_mut() {
|
||||
if let Some(monocle) = focused_workspace.monocle_container() {
|
||||
if let Some(window) = focused_workspace.maximized_window() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
// (alex-ds13): @LGUG2Z Why was this being done below on the monocle?
|
||||
// Should it really be done?
|
||||
//
|
||||
// WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
|
||||
// window.hwnd,
|
||||
// )?)?;
|
||||
|
||||
cross_monitor_monocle_or_max = true;
|
||||
} else if let Some(monocle) = focused_workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
|
||||
window.hwnd,
|
||||
)?)?;
|
||||
|
||||
cross_monitor_monocle = true;
|
||||
cross_monitor_monocle_or_max = true;
|
||||
}
|
||||
} else {
|
||||
match direction {
|
||||
@@ -1508,7 +1924,7 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
if !cross_monitor_monocle {
|
||||
if !cross_monitor_monocle_or_max {
|
||||
if let Ok(focused_window) = self.focused_window_mut() {
|
||||
focused_window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
@@ -1587,15 +2003,13 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
|
||||
|
||||
{
|
||||
// remove the container from the origin monitor workspace
|
||||
let origin_container = self
|
||||
.focused_workspace_mut()?
|
||||
.remove_container_by_idx(origin_container_idx)
|
||||
.ok_or_else(|| {
|
||||
anyhow!("could not remove container at given origin index")
|
||||
})?;
|
||||
|
||||
self.focused_workspace_mut()?.focus_previous_container();
|
||||
// actually move the container to target monitor using the direction
|
||||
self.move_container_to_monitor(
|
||||
target_monitor_idx,
|
||||
None,
|
||||
true,
|
||||
Some(direction),
|
||||
)?;
|
||||
|
||||
// focus the target monitor
|
||||
self.focus_monitor(target_monitor_idx)?;
|
||||
@@ -1615,79 +2029,6 @@ impl WindowManager {
|
||||
// get a mutable ref to the focused workspace on the target monitor
|
||||
let target_workspace = self.focused_workspace_mut()?;
|
||||
|
||||
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
|
||||
// focused position
|
||||
@@ -1832,7 +2173,12 @@ impl WindowManager {
|
||||
|
||||
tracing::info!("cycling container windows");
|
||||
|
||||
let container = self.focused_container_mut()?;
|
||||
let container =
|
||||
if let Some(container) = self.focused_workspace_mut()?.monocle_container_mut() {
|
||||
container
|
||||
} else {
|
||||
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"))?;
|
||||
@@ -1851,12 +2197,20 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn focus_container_window(&mut self, idx: usize) -> Result<()> {
|
||||
pub fn cycle_container_window_index_in_direction(
|
||||
&mut self,
|
||||
direction: CycleDirection,
|
||||
) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
tracing::info!("focusing container window at index {idx}");
|
||||
tracing::info!("cycling container window index");
|
||||
|
||||
let container = self.focused_container_mut()?;
|
||||
let container =
|
||||
if let Some(container) = self.focused_workspace_mut()?.monocle_container_mut() {
|
||||
container
|
||||
} else {
|
||||
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"))?;
|
||||
@@ -1865,6 +2219,36 @@ impl WindowManager {
|
||||
bail!("there is only one window in this container");
|
||||
}
|
||||
|
||||
let current_idx = container.focused_window_idx();
|
||||
let next_idx = direction.next_idx(current_idx, len);
|
||||
container.windows_mut().swap(current_idx, next_idx);
|
||||
|
||||
container.focus_window(next_idx);
|
||||
container.load_focused_window();
|
||||
|
||||
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 =
|
||||
if let Some(container) = self.focused_workspace_mut()?.monocle_container_mut() {
|
||||
container
|
||||
} else {
|
||||
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 && idx != 0 {
|
||||
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}");
|
||||
}
|
||||
@@ -1972,8 +2356,16 @@ impl WindowManager {
|
||||
new_idx
|
||||
};
|
||||
|
||||
let mut target_container_is_stack = false;
|
||||
|
||||
if let Some(container) = workspace.containers().get(adjusted_new_index) {
|
||||
if container.windows().len() > 1 {
|
||||
target_container_is_stack = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(current) = workspace.focused_container() {
|
||||
if current.windows().len() > 1 {
|
||||
if current.windows().len() > 1 && !target_container_is_stack {
|
||||
workspace.focus_container(adjusted_new_index);
|
||||
workspace.move_window_to_container(current_container_idx)?;
|
||||
} else {
|
||||
@@ -2878,6 +3270,10 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no container"))
|
||||
}
|
||||
|
||||
pub fn focused_container_idx(&self) -> Result<usize> {
|
||||
Ok(self.focused_workspace()?.focused_container_idx())
|
||||
}
|
||||
|
||||
pub fn focused_container_mut(&mut self) -> Result<&mut Container> {
|
||||
self.focused_workspace_mut()?
|
||||
.focused_container_mut()
|
||||
|
||||
@@ -77,6 +77,7 @@ use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetLayeredWindowAttributes;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetTopWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||
@@ -105,7 +106,6 @@ use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
|
||||
@@ -128,7 +128,6 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
|
||||
@@ -153,6 +152,7 @@ macro_rules! as_ptr {
|
||||
};
|
||||
}
|
||||
|
||||
use crate::border_manager::Border;
|
||||
pub(crate) use as_ptr;
|
||||
|
||||
pub enum WindowsResult<T, E> {
|
||||
@@ -300,14 +300,21 @@ impl WindowsApi {
|
||||
}
|
||||
}
|
||||
|
||||
if monitors.elements().is_empty() {
|
||||
monitors.elements_mut().push_back(m);
|
||||
} else if let Some(preference) = index_preference {
|
||||
while *preference > monitors.elements().len() {
|
||||
if let Some(preference) = index_preference {
|
||||
while *preference >= monitors.elements().len() {
|
||||
monitors.elements_mut().push_back(Monitor::placeholder());
|
||||
}
|
||||
|
||||
monitors.elements_mut().insert(*preference, m);
|
||||
let current_name = monitors
|
||||
.elements_mut()
|
||||
.get(*preference)
|
||||
.map_or("", |m| m.name());
|
||||
if current_name == "PLACEHOLDER" {
|
||||
let _ = monitors.elements_mut().remove(*preference);
|
||||
monitors.elements_mut().insert(*preference, m);
|
||||
} else {
|
||||
monitors.elements_mut().insert(*preference, m);
|
||||
}
|
||||
} else {
|
||||
monitors.elements_mut().push_back(m);
|
||||
}
|
||||
@@ -445,7 +452,13 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn set_border_pos(hwnd: isize, layout: &Rect, position: isize) -> Result<()> {
|
||||
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
|
||||
let flags = {
|
||||
SetWindowPosition::NO_SEND_CHANGING
|
||||
| SetWindowPosition::NO_ACTIVATE
|
||||
| SetWindowPosition::NO_REDRAW
|
||||
| SetWindowPosition::SHOW_WINDOW
|
||||
};
|
||||
|
||||
Self::set_window_pos(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
layout,
|
||||
@@ -1082,10 +1095,14 @@ impl WindowsApi {
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn create_border_window(name: PCWSTR, instance: isize) -> Result<isize> {
|
||||
pub fn create_border_window(
|
||||
name: PCWSTR,
|
||||
instance: isize,
|
||||
border: *const Border,
|
||||
) -> Result<isize> {
|
||||
unsafe {
|
||||
let hwnd = CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
|
||||
CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
|
||||
name,
|
||||
name,
|
||||
WS_POPUP | WS_SYSMENU,
|
||||
@@ -1096,12 +1113,8 @@ impl WindowsApi {
|
||||
None,
|
||||
None,
|
||||
HINSTANCE(as_ptr!(instance)),
|
||||
None,
|
||||
)?;
|
||||
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
|
||||
|
||||
hwnd
|
||||
Some(border as _),
|
||||
)?
|
||||
}
|
||||
.process()
|
||||
}
|
||||
@@ -1120,6 +1133,21 @@ impl WindowsApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_transparent(hwnd: isize) -> Result<u8> {
|
||||
unsafe {
|
||||
let mut alpha: u8 = u8::default();
|
||||
let mut color_ref = COLORREF(-1i32 as u32);
|
||||
let mut flags = LWA_ALPHA;
|
||||
GetLayeredWindowAttributes(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
Some(std::ptr::addr_of_mut!(color_ref)),
|
||||
Some(std::ptr::addr_of_mut!(alpha)),
|
||||
Some(std::ptr::addr_of_mut!(flags)),
|
||||
)?;
|
||||
Ok(alpha)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_hidden_window(name: PCWSTR, instance: isize) -> Result<isize> {
|
||||
unsafe {
|
||||
CreateWindowExW(
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::container::Container;
|
||||
use crate::window::RuleDebug;
|
||||
use crate::window::Window;
|
||||
@@ -12,6 +8,19 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::winevent_listener;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SendNotifyMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::OBJID_WINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
|
||||
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
|
||||
@@ -60,6 +69,15 @@ pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
true.into()
|
||||
}
|
||||
|
||||
fn has_filtered_style(hwnd: HWND) -> bool {
|
||||
let style = unsafe { GetWindowLongW(hwnd, GWL_STYLE) as u32 };
|
||||
let ex_style = unsafe { GetWindowLongW(hwnd, GWL_EXSTYLE) as u32 };
|
||||
|
||||
style & WS_CHILD.0 != 0
|
||||
|| ex_style & WS_EX_TOOLWINDOW.0 != 0
|
||||
|| ex_style & WS_EX_NOACTIVATE.0 != 0
|
||||
}
|
||||
|
||||
pub extern "system" fn win_event_hook(
|
||||
_h_win_event_hook: HWINEVENTHOOK,
|
||||
event: u32,
|
||||
@@ -69,8 +87,7 @@ pub extern "system" fn win_event_hook(
|
||||
_id_event_thread: u32,
|
||||
_dwms_event_time: u32,
|
||||
) {
|
||||
// OBJID_WINDOW
|
||||
if id_object != 0 {
|
||||
if id_object != OBJID_WINDOW.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,6 +98,23 @@ pub extern "system" fn win_event_hook(
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// this forwards the message to the window's border when it moves or is destroyed
|
||||
// see border_manager/border.rs
|
||||
if matches!(
|
||||
winevent,
|
||||
WinEvent::ObjectLocationChange | WinEvent::ObjectDestroy
|
||||
) && !has_filtered_style(hwnd)
|
||||
{
|
||||
let border_window = border_manager::window_border(hwnd.0 as isize);
|
||||
|
||||
if let Some(border) = border_window {
|
||||
unsafe {
|
||||
let _ =
|
||||
SendNotifyMessageW(border.hwnd(), event, WPARAM(0), LPARAM(hwnd.0 as isize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
|
||||
None => {
|
||||
tracing::trace!(
|
||||
|
||||
@@ -11,6 +11,8 @@ use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_MAX;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_MIN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WINEVENT_OUTOFCONTEXT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WINEVENT_SKIPOWNPROCESS;
|
||||
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_callbacks;
|
||||
@@ -31,7 +33,7 @@ pub fn start() {
|
||||
Some(windows_callbacks::win_event_hook),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS,
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::should_act;
|
||||
use crate::stackbar_manager;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
|
||||
use crate::static_config::WorkspaceConfig;
|
||||
@@ -35,6 +36,7 @@ use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
|
||||
#[allow(clippy::struct_field_names)]
|
||||
@@ -117,6 +119,14 @@ impl Default for Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WorkspaceWindowLocation {
|
||||
Monocle(usize), // window_idx
|
||||
Maximized,
|
||||
Container(usize, usize), // container_idx, window_idx
|
||||
Floating(usize), // idx in floating_windows
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn load_static_config(&mut self, config: &WorkspaceConfig) -> Result<()> {
|
||||
self.name = Option::from(config.name.clone());
|
||||
@@ -246,6 +256,10 @@ impl Workspace {
|
||||
if let Some(window) = to_focus {
|
||||
if self.maximized_window().is_none() && self.floating_windows().is_empty() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
} else if let Some(maximized_window) = self.maximized_window() {
|
||||
maximized_window.focus(mouse_follows_focus)?;
|
||||
} else if let Some(floating_window) = self.floating_windows().first() {
|
||||
floating_window.focus(mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +364,7 @@ impl Workspace {
|
||||
|
||||
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
let no_titlebar = NO_TITLEBAR.lock().clone();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock().clone();
|
||||
|
||||
let container_padding = self.container_padding().unwrap_or(0);
|
||||
let containers = self.containers_mut();
|
||||
@@ -357,21 +372,7 @@ impl Workspace {
|
||||
for (i, container) in containers.iter_mut().enumerate() {
|
||||
let window_count = container.windows().len();
|
||||
|
||||
if let (Some(window), Some(layout)) =
|
||||
(container.focused_window_mut(), layouts.get_mut(i))
|
||||
{
|
||||
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
|
||||
window.remove_title_bar()?;
|
||||
} else if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
// If a window has been unmaximized via toggle-maximize, this block
|
||||
// will make sure that it is unmaximized via restore_window
|
||||
if window.is_maximized() && !managed_maximized_window {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
|
||||
if let Some(layout) = layouts.get_mut(i) {
|
||||
{
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
layout.add_padding(border_offset);
|
||||
@@ -388,7 +389,35 @@ impl Workspace {
|
||||
layout.bottom -= total_height;
|
||||
}
|
||||
|
||||
window.set_position(layout, false)?;
|
||||
for window in container.windows() {
|
||||
if container
|
||||
.focused_window()
|
||||
.is_some_and(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
let should_remove_titlebar_for_window = should_act(
|
||||
&window.title().unwrap_or_default(),
|
||||
&window.exe().unwrap_or_default(),
|
||||
&window.class().unwrap_or_default(),
|
||||
&window.path().unwrap_or_default(),
|
||||
&no_titlebar,
|
||||
®ex_identifiers,
|
||||
)
|
||||
.is_some();
|
||||
|
||||
if should_remove_titlebars && should_remove_titlebar_for_window {
|
||||
window.remove_title_bar()?;
|
||||
} else if should_remove_titlebar_for_window {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
// If a window has been unmaximized via toggle-maximize, this block
|
||||
// will make sure that it is unmaximized via restore_window
|
||||
if window.is_maximized() && !managed_maximized_window {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
window.set_position(layout, false)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,7 +460,7 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
for window in self.visible_windows_mut().into_iter().flatten() {
|
||||
for window in self.visible_windows().into_iter().flatten() {
|
||||
if !window.is_window() {
|
||||
hwnds.push(window.hwnd);
|
||||
}
|
||||
@@ -558,6 +587,41 @@ impl Workspace {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn location_from_exe(&self, exe: &str) -> Option<WorkspaceWindowLocation> {
|
||||
for (container_idx, container) in self.containers().iter().enumerate() {
|
||||
if let Some(window_idx) = container.idx_from_exe(exe) {
|
||||
return Some(WorkspaceWindowLocation::Container(
|
||||
container_idx,
|
||||
window_idx,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = self.maximized_window() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Some(WorkspaceWindowLocation::Maximized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(container) = self.monocle_container() {
|
||||
if let Some(window_idx) = container.idx_from_exe(exe) {
|
||||
return Some(WorkspaceWindowLocation::Monocle(window_idx));
|
||||
}
|
||||
}
|
||||
|
||||
for (window_idx, window) in self.floating_windows().iter().enumerate() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Some(WorkspaceWindowLocation::Floating(window_idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn contains_managed_window(&self, hwnd: isize) -> bool {
|
||||
for container in self.containers() {
|
||||
if container.contains_window(hwnd) {
|
||||
@@ -1409,16 +1473,41 @@ impl Workspace {
|
||||
|
||||
pub fn visible_windows(&self) -> Vec<Option<&Window>> {
|
||||
let mut vec = vec![];
|
||||
|
||||
vec.push(self.maximized_window().as_ref());
|
||||
|
||||
if let Some(monocle) = self.monocle_container() {
|
||||
vec.push(monocle.focused_window());
|
||||
}
|
||||
|
||||
for container in self.containers() {
|
||||
vec.push(container.focused_window());
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
vec.push(Some(window));
|
||||
}
|
||||
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn visible_window_details(&self) -> Vec<WindowDetails> {
|
||||
let mut vec: Vec<WindowDetails> = vec![];
|
||||
|
||||
if let Some(maximized) = self.maximized_window() {
|
||||
if let Ok(details) = (*maximized).try_into() {
|
||||
vec.push(details);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(monocle) = self.monocle_container() {
|
||||
if let Some(focused) = monocle.focused_window() {
|
||||
if let Ok(details) = (*focused).try_into() {
|
||||
vec.push(details);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for container in self.containers() {
|
||||
if let Some(focused) = container.focused_window() {
|
||||
if let Ok(details) = (*focused).try_into() {
|
||||
@@ -1427,13 +1516,10 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn visible_windows_mut(&mut self) -> Vec<Option<&mut Window>> {
|
||||
let mut vec = vec![];
|
||||
for container in self.containers_mut() {
|
||||
vec.push(container.focused_window_mut());
|
||||
for window in self.floating_windows() {
|
||||
if let Ok(details) = (*window).try_into() {
|
||||
vec.push(details);
|
||||
}
|
||||
}
|
||||
|
||||
vec
|
||||
|
||||
@@ -88,6 +88,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let mouse_follows_focus = wm.mouse_follows_focus;
|
||||
|
||||
if let Some(monitor) = wm.focused_monitor_mut() {
|
||||
let previous_idx = monitor.focused_workspace_idx();
|
||||
monitor.set_last_focused_workspace(Option::from(previous_idx));
|
||||
monitor.focus_workspace(notification.workspace_idx)?;
|
||||
monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
[package]
|
||||
name = "komorebic-no-console"
|
||||
version = "0.1.30"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
version = "0.1.32"
|
||||
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
||||
categories = ["cli", "tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.30"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
version = "0.1.32"
|
||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||
categories = ["cli", "tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -30,7 +28,7 @@ serde_json = { workspace = true }
|
||||
serde_yaml = "0.9"
|
||||
shadow-rs = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
thiserror = "1"
|
||||
thiserror = "2"
|
||||
uds_windows = { workspace = true }
|
||||
which = { workspace = true }
|
||||
win32-display-data = { workspace = true }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
fn main() {
|
||||
if std::fs::metadata("applications.yaml").is_err() {
|
||||
let applications_yaml = reqwest::blocking::get(
|
||||
"https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.yaml"
|
||||
if std::fs::metadata("applications.json").is_err() {
|
||||
let applications_json = reqwest::blocking::get(
|
||||
"https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.json"
|
||||
).unwrap().text().unwrap();
|
||||
std::fs::write("applications.yaml", applications_yaml).unwrap();
|
||||
std::fs::write("applications.json", applications_json).unwrap();
|
||||
}
|
||||
|
||||
shadow_rs::new().unwrap();
|
||||
|
||||
@@ -160,6 +160,7 @@ gen_enum_subcommand_args! {
|
||||
CycleMoveWorkspaceToMonitor: CycleDirection,
|
||||
Stack: OperationDirection,
|
||||
CycleStack: CycleDirection,
|
||||
CycleStackIndex: CycleDirection,
|
||||
FlipLayout: Axis,
|
||||
ChangeLayout: DefaultLayout,
|
||||
CycleLayout: CycleDirection,
|
||||
@@ -723,12 +724,18 @@ struct BorderImplementation {
|
||||
struct Animation {
|
||||
#[clap(value_enum)]
|
||||
boolean_state: BooleanState,
|
||||
/// Animation type to apply the state to. If not specified, sets global state
|
||||
#[clap(value_enum, short, long)]
|
||||
animation_type: Option<komorebi_client::AnimationPrefix>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct AnimationDuration {
|
||||
/// Desired animation durations in ms
|
||||
duration: u64,
|
||||
/// Animation type to apply the duration to. If not specified, sets global duration
|
||||
#[clap(value_enum, short, long)]
|
||||
animation_type: Option<komorebi_client::AnimationPrefix>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -742,12 +749,16 @@ struct AnimationStyle {
|
||||
/// Desired ease function for animation
|
||||
#[clap(value_enum, short, long, default_value = "linear")]
|
||||
style: komorebi_client::AnimationStyle,
|
||||
/// Animation type to apply the style to. If not specified, sets global style
|
||||
#[clap(value_enum, short, long)]
|
||||
animation_type: Option<komorebi_client::AnimationPrefix>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct Start {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(hide = true)]
|
||||
#[clap(short, long = "ffm")]
|
||||
ffm: bool,
|
||||
/// Path to a static configuration JSON file
|
||||
@@ -768,6 +779,12 @@ struct Start {
|
||||
/// Start komorebi-bar in a background process
|
||||
#[clap(long)]
|
||||
bar: bool,
|
||||
/// Start masir in a background process for focus-follows-mouse
|
||||
#[clap(long)]
|
||||
masir: bool,
|
||||
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
|
||||
#[clap(long)]
|
||||
clean_state: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -775,9 +792,34 @@ struct Stop {
|
||||
/// Stop whkd if it is running as a background process
|
||||
#[clap(long)]
|
||||
whkd: bool,
|
||||
/// Stop ahk if it is running as a background process
|
||||
#[clap(long)]
|
||||
ahk: bool,
|
||||
/// Stop komorebi-bar if it is running as a background process
|
||||
#[clap(long)]
|
||||
bar: bool,
|
||||
/// Stop masir if it is running as a background process
|
||||
#[clap(long)]
|
||||
masir: bool,
|
||||
/// Do not restore windows after stopping komorebi
|
||||
#[clap(long, hide = true)]
|
||||
ignore_restore: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Kill {
|
||||
/// Kill whkd if it is running as a background process
|
||||
#[clap(long)]
|
||||
whkd: bool,
|
||||
/// Kill ahk if it is running as a background process
|
||||
#[clap(long)]
|
||||
ahk: bool,
|
||||
/// Kill komorebi-bar if it is running as a background process
|
||||
#[clap(long)]
|
||||
bar: bool,
|
||||
/// Kill masir if it is running as a background process
|
||||
#[clap(long)]
|
||||
masir: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -862,6 +904,7 @@ struct EnableAutostart {
|
||||
#[clap(action, short, long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Enable komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(hide = true)]
|
||||
#[clap(short, long = "ffm")]
|
||||
ffm: bool,
|
||||
/// Enable autostart of whkd
|
||||
@@ -873,6 +916,9 @@ struct EnableAutostart {
|
||||
/// Enable autostart of komorebi-bar
|
||||
#[clap(long)]
|
||||
bar: bool,
|
||||
/// Enable autostart of masir
|
||||
#[clap(long)]
|
||||
masir: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -881,6 +927,12 @@ struct ReplaceConfiguration {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct EagerFocus {
|
||||
/// Case-sensitive exe identifier
|
||||
exe: String,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
||||
struct Opts {
|
||||
@@ -898,6 +950,8 @@ enum SubCommand {
|
||||
Start(Start),
|
||||
/// Stop the komorebi.exe process and restore all hidden windows
|
||||
Stop(Stop),
|
||||
/// Kill background processes started by komorebic
|
||||
Kill(Kill),
|
||||
/// Check komorebi configuration and related files for common errors
|
||||
Check,
|
||||
/// Show the path to komorebi.json
|
||||
@@ -972,6 +1026,9 @@ enum SubCommand {
|
||||
/// Move the focused window in the specified cycle direction
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleMove(CycleMove),
|
||||
/// Focus the first managed window matching the given exe
|
||||
#[clap(arg_required_else_help = true)]
|
||||
EagerFocus(EagerFocus),
|
||||
/// Stack the focused window in the specified direction
|
||||
#[clap(arg_required_else_help = true)]
|
||||
Stack(Stack),
|
||||
@@ -980,6 +1037,9 @@ enum SubCommand {
|
||||
/// Cycle the focused stack in the specified cycle direction
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleStack(CycleStack),
|
||||
/// Cycle the index of the focused window in the focused stack in the specified cycle direction
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleStackIndex(CycleStackIndex),
|
||||
/// Focus the specified window index in the focused stack
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusStackWindow(FocusStackWindow),
|
||||
@@ -1047,6 +1107,8 @@ enum SubCommand {
|
||||
/// Focus the specified workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusNamedWorkspace(FocusNamedWorkspace),
|
||||
/// Close the focused workspace (must be empty and unnamed)
|
||||
CloseWorkspace,
|
||||
/// Focus the monitor in the given cycle direction
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleMonitor(CycleMonitor),
|
||||
@@ -1095,6 +1157,7 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleLayout(CycleLayout),
|
||||
/// Load a custom layout from file for the focused workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
LoadCustomLayout(LoadCustomLayout),
|
||||
/// Flip the layout on the focused workspace (BSP only)
|
||||
@@ -1139,9 +1202,11 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
NamedWorkspaceLayout(NamedWorkspaceLayout),
|
||||
/// Set a custom layout for the specified workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
WorkspaceCustomLayout(WorkspaceCustomLayout),
|
||||
/// Set a custom layout for the specified workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
NamedWorkspaceCustomLayout(NamedWorkspaceCustomLayout),
|
||||
/// Add a dynamic layout rule for the specified workspace
|
||||
@@ -1151,9 +1216,11 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
NamedWorkspaceLayoutRule(NamedWorkspaceLayoutRule),
|
||||
/// Add a dynamic custom layout for the specified workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
WorkspaceCustomLayoutRule(WorkspaceCustomLayoutRule),
|
||||
/// Add a dynamic custom layout for the specified workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
NamedWorkspaceCustomLayoutRule(NamedWorkspaceCustomLayoutRule),
|
||||
/// Clear all dynamic layout rules for the specified workspace
|
||||
@@ -1251,6 +1318,8 @@ enum SubCommand {
|
||||
ClearNamedWorkspaceRules(ClearNamedWorkspaceRules),
|
||||
/// Remove all application association rules for all workspaces
|
||||
ClearAllWorkspaceRules,
|
||||
/// Enforce all workspace rules, including initial workspace rules that have already been applied
|
||||
EnforceWorkspaceRules,
|
||||
/// Identify an application that sends EVENT_OBJECT_NAMECHANGE on launch
|
||||
#[clap(arg_required_else_help = true)]
|
||||
IdentifyObjectNameChangeApplication(IdentifyObjectNameChangeApplication),
|
||||
@@ -1312,9 +1381,11 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
AnimationStyle(AnimationStyle),
|
||||
/// Enable or disable focus follows mouse for the operating system
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusFollowsMouse(FocusFollowsMouse),
|
||||
/// Toggle focus follows mouse for the operating system
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
ToggleFocusFollowsMouse(ToggleFocusFollowsMouse),
|
||||
/// Enable or disable mouse follows focus on all workspaces
|
||||
@@ -1337,11 +1408,12 @@ enum SubCommand {
|
||||
/// Format a YAML file for use with the 'app-specific-configuration' command
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "fmt-asc")]
|
||||
#[clap(hide = true)]
|
||||
FormatAppSpecificConfiguration(FormatAppSpecificConfiguration),
|
||||
/// Fetch the latest version of applications.yaml from komorebi-application-specific-configuration
|
||||
/// Fetch the latest version of applications.json from komorebi-application-specific-configuration
|
||||
#[clap(alias = "fetch-asc")]
|
||||
FetchAppSpecificConfiguration,
|
||||
/// Generate a JSON Schema for applications.yaml
|
||||
/// Generate a JSON Schema for applications.json
|
||||
#[clap(alias = "asc-schema")]
|
||||
ApplicationSpecificConfigurationSchema,
|
||||
/// Generate a JSON Schema of subscription notifications
|
||||
@@ -1399,6 +1471,14 @@ fn main() -> Result<()> {
|
||||
"docgen",
|
||||
"alt-focus-hack",
|
||||
"identify-border-overflow-application",
|
||||
"load-custom-layout",
|
||||
"workspace-custom-layout",
|
||||
"named-workspace-custom-layout",
|
||||
"workspace-custom-layout-rule",
|
||||
"named-workspace-custom-layout-rule",
|
||||
"focus-follows-mouse",
|
||||
"toggle-focus-follows-mouse",
|
||||
"format-app-specific-configuration",
|
||||
];
|
||||
|
||||
for cmd in subcommands {
|
||||
@@ -1431,13 +1511,13 @@ fn main() -> Result<()> {
|
||||
std::fs::write(HOME_DIR.join("komorebi.json"), komorebi_json)?;
|
||||
std::fs::write(HOME_DIR.join("komorebi.bar.json"), komorebi_bar_json)?;
|
||||
|
||||
let applications_yaml = include_str!("../applications.yaml");
|
||||
std::fs::write(HOME_DIR.join("applications.yaml"), applications_yaml)?;
|
||||
let applications_json = include_str!("../applications.json");
|
||||
std::fs::write(HOME_DIR.join("applications.json"), applications_json)?;
|
||||
|
||||
let whkdrc = include_str!("../../docs/whkdrc.sample");
|
||||
std::fs::write(WHKD_CONFIG_DIR.join("whkdrc"), whkdrc)?;
|
||||
|
||||
println!("Example komorebi.json, komorebi.bar.json, whkdrc and latest applications.yaml files created");
|
||||
println!("Example komorebi.json, komorebi.bar.json, whkdrc and latest applications.json files created");
|
||||
println!("You can now run komorebic start --whkd --bar");
|
||||
}
|
||||
SubCommand::EnableAutostart(args) => {
|
||||
@@ -1471,6 +1551,10 @@ fn main() -> Result<()> {
|
||||
arguments.push_str(" --ahk");
|
||||
}
|
||||
|
||||
if args.masir {
|
||||
arguments.push_str(" --masir");
|
||||
}
|
||||
|
||||
Command::new("powershell")
|
||||
.arg("-c")
|
||||
.arg("$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut($env:SHORTCUT_PATH); $Shortcut.TargetPath = $env:TARGET_PATH; $Shortcut.Arguments = $env:TARGET_ARGS; $Shortcut.Save()")
|
||||
@@ -1651,6 +1735,9 @@ fn main() -> Result<()> {
|
||||
SubCommand::CycleMove(arg) => {
|
||||
send_message(&SocketMessage::CycleMoveWindow(arg.cycle_direction))?;
|
||||
}
|
||||
SubCommand::EagerFocus(arg) => {
|
||||
send_message(&SocketMessage::EagerFocus(arg.exe))?;
|
||||
}
|
||||
SubCommand::MoveToMonitor(arg) => {
|
||||
send_message(&SocketMessage::MoveContainerToMonitorNumber(arg.target))?;
|
||||
}
|
||||
@@ -1891,6 +1978,10 @@ fn main() -> Result<()> {
|
||||
bail!("could not find whkd, please make sure it is installed before using the --whkd flag");
|
||||
}
|
||||
|
||||
if arg.masir && which("masir").is_err() {
|
||||
bail!("could not find masir, please make sure it is installed before using the --masir flag");
|
||||
}
|
||||
|
||||
if arg.ahk && which(&ahk).is_err() {
|
||||
bail!("could not find autohotkey, please make sure it is installed before using the --ahk flag");
|
||||
}
|
||||
@@ -1941,6 +2032,10 @@ fn main() -> Result<()> {
|
||||
flags.push(format!("'--tcp-port={port}'"));
|
||||
}
|
||||
|
||||
if arg.clean_state {
|
||||
flags.push("'--clean-state'".to_string());
|
||||
}
|
||||
|
||||
let script = if flags.is_empty() {
|
||||
format!(
|
||||
"Start-Process '{}' -WindowStyle hidden",
|
||||
@@ -1954,8 +2049,14 @@ fn main() -> Result<()> {
|
||||
)
|
||||
};
|
||||
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes(ProcessesToUpdate::All);
|
||||
|
||||
let mut attempts = 0;
|
||||
let mut running = false;
|
||||
let mut running = system
|
||||
.processes_by_name("komorebi.exe".as_ref())
|
||||
.next()
|
||||
.is_some();
|
||||
|
||||
while !running && attempts <= 2 {
|
||||
match powershell_script::run(&script) {
|
||||
@@ -1970,7 +2071,6 @@ fn main() -> Result<()> {
|
||||
print!("Waiting for komorebi.exe to start...");
|
||||
std::thread::sleep(Duration::from_secs(3));
|
||||
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes(ProcessesToUpdate::All);
|
||||
|
||||
if system
|
||||
@@ -2058,7 +2158,7 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
let mut config = StaticConfig::read(config)?;
|
||||
if let Some(display_bar_configurations) = &mut config.bar_configurations {
|
||||
for config_file_path in &mut *display_bar_configurations {
|
||||
let script = r"Start-Process 'komorebi-bar' '--config CONFIGFILE' -WindowStyle hidden"
|
||||
let script = r#"Start-Process "komorebi-bar" '"--config" "CONFIGFILE"' -WindowStyle hidden"#
|
||||
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
|
||||
|
||||
match powershell_script::run(&script) {
|
||||
@@ -2089,12 +2189,36 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
|
||||
}
|
||||
}
|
||||
|
||||
if arg.masir {
|
||||
let script = r"
|
||||
if (!(Get-Process masir -ErrorAction SilentlyContinue))
|
||||
{
|
||||
Start-Process masir -WindowStyle hidden
|
||||
}
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nThank you for using komorebi!\n");
|
||||
println!("* Become a sponsor https://github.com/sponsors/LGUG2Z - Even $1/month makes a big difference");
|
||||
println!(
|
||||
"* Subscribe to https://youtube.com/@LGUG2Z - Live dev videos and feature previews"
|
||||
);
|
||||
println!("# Commercial Use License");
|
||||
println!("* View licensing options https://lgug2z.com/software/komorebi - A commercial use license is required to use komorebi at work");
|
||||
println!("\n# Personal Use Sponsorship");
|
||||
println!("* Become a sponsor https://github.com/sponsors/LGUG2Z - $5/month makes a big difference");
|
||||
println!("* Leave a tip https://ko-fi.com/lgug2z - An alternative to GitHub Sponsors");
|
||||
println!("\n# Community");
|
||||
println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, share your desktops");
|
||||
println!(
|
||||
"* Subscribe to https://youtube.com/@LGUG2Z - Development videos, feature previews and release overviews"
|
||||
);
|
||||
println!("* Explore the Awesome Komorebi list https://github.com/LGUG2Z/awesome-komorebi - Projects in the komorebi ecosystem");
|
||||
println!("\n# Documentation");
|
||||
println!("* Read the docs https://lgug2z.github.io/komorebi - Quickly search through all komorebic commands");
|
||||
|
||||
let bar_config = arg.config.map_or_else(
|
||||
@@ -2152,7 +2276,54 @@ Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
send_message(&SocketMessage::Stop)?;
|
||||
if arg.masir {
|
||||
let script = r"
|
||||
Stop-Process -Name:masir -ErrorAction SilentlyContinue
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if arg.ahk {
|
||||
let script = r#"
|
||||
if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
||||
(Get-CimInstance Win32_Process | Where-Object {
|
||||
($_.CommandLine -like '*komorebi.ahk"') -and
|
||||
($_.Name -in @('AutoHotkey.exe', 'AutoHotkey64.exe', 'AutoHotkey32.exe', 'AutoHotkeyUX.exe'))
|
||||
} | Select-Object -First 1) | ForEach-Object {
|
||||
Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue
|
||||
}
|
||||
} else {
|
||||
(Get-WmiObject Win32_Process | Where-Object {
|
||||
($_.CommandLine -like '*komorebi.ahk"') -and
|
||||
($_.Name -in @('AutoHotkey.exe', 'AutoHotkey64.exe', 'AutoHotkey32.exe', 'AutoHotkeyUX.exe'))
|
||||
} | Select-Object -First 1) | ForEach-Object {
|
||||
Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if arg.ignore_restore {
|
||||
send_message(&SocketMessage::StopIgnoreRestore)?;
|
||||
} else {
|
||||
send_message(&SocketMessage::Stop)?;
|
||||
}
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes(ProcessesToUpdate::All);
|
||||
|
||||
@@ -2182,6 +2353,78 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
SubCommand::Kill(arg) => {
|
||||
if arg.whkd {
|
||||
let script = r"
|
||||
Stop-Process -Name:whkd -ErrorAction SilentlyContinue
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if arg.bar {
|
||||
let script = r"
|
||||
Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if arg.masir {
|
||||
let script = r"
|
||||
Stop-Process -Name:masir -ErrorAction SilentlyContinue
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if arg.ahk {
|
||||
let script = r#"
|
||||
if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
||||
(Get-CimInstance Win32_Process | Where-Object {
|
||||
($_.CommandLine -like '*komorebi.ahk"') -and
|
||||
($_.Name -in @('AutoHotkey.exe', 'AutoHotkey64.exe', 'AutoHotkey32.exe', 'AutoHotkeyUX.exe'))
|
||||
} | Select-Object -First 1) | ForEach-Object {
|
||||
Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue
|
||||
}
|
||||
} else {
|
||||
(Get-WmiObject Win32_Process | Where-Object {
|
||||
($_.CommandLine -like '*komorebi.ahk"') -and
|
||||
($_.Name -in @('AutoHotkey.exe', 'AutoHotkey64.exe', 'AutoHotkey32.exe', 'AutoHotkeyUX.exe'))
|
||||
} | Select-Object -First 1) | ForEach-Object {
|
||||
Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SubCommand::IgnoreRule(arg) => {
|
||||
send_message(&SocketMessage::IgnoreRule(arg.identifier, arg.id))?;
|
||||
}
|
||||
@@ -2230,6 +2473,9 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
SubCommand::ClearAllWorkspaceRules => {
|
||||
send_message(&SocketMessage::ClearAllWorkspaceRules)?;
|
||||
}
|
||||
SubCommand::EnforceWorkspaceRules => {
|
||||
send_message(&SocketMessage::EnforceWorkspaceRules)?;
|
||||
}
|
||||
SubCommand::Stack(arg) => {
|
||||
send_message(&SocketMessage::StackWindow(arg.operation_direction))?;
|
||||
}
|
||||
@@ -2248,6 +2494,9 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
SubCommand::CycleStack(arg) => {
|
||||
send_message(&SocketMessage::CycleStack(arg.cycle_direction))?;
|
||||
}
|
||||
SubCommand::CycleStackIndex(arg) => {
|
||||
send_message(&SocketMessage::CycleStackIndex(arg.cycle_direction))?;
|
||||
}
|
||||
SubCommand::ChangeLayout(arg) => {
|
||||
send_message(&SocketMessage::ChangeLayout(arg.default_layout))?;
|
||||
}
|
||||
@@ -2283,6 +2532,9 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
SubCommand::FocusNamedWorkspace(arg) => {
|
||||
send_message(&SocketMessage::FocusNamedWorkspace(arg.workspace))?;
|
||||
}
|
||||
SubCommand::CloseWorkspace => {
|
||||
send_message(&SocketMessage::CloseWorkspace)?;
|
||||
}
|
||||
SubCommand::CycleMonitor(arg) => {
|
||||
send_message(&SocketMessage::CycleFocusMonitor(arg.cycle_direction))?;
|
||||
}
|
||||
@@ -2479,16 +2731,25 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
send_message(&SocketMessage::ToggleTransparency)?;
|
||||
}
|
||||
SubCommand::Animation(arg) => {
|
||||
send_message(&SocketMessage::Animation(arg.boolean_state.into()))?;
|
||||
send_message(&SocketMessage::Animation(
|
||||
arg.boolean_state.into(),
|
||||
arg.animation_type,
|
||||
))?;
|
||||
}
|
||||
SubCommand::AnimationDuration(arg) => {
|
||||
send_message(&SocketMessage::AnimationDuration(arg.duration))?;
|
||||
send_message(&SocketMessage::AnimationDuration(
|
||||
arg.duration,
|
||||
arg.animation_type,
|
||||
))?;
|
||||
}
|
||||
SubCommand::AnimationFps(arg) => {
|
||||
send_message(&SocketMessage::AnimationFps(arg.fps))?;
|
||||
}
|
||||
SubCommand::AnimationStyle(arg) => {
|
||||
send_message(&SocketMessage::AnimationStyle(arg.style))?;
|
||||
send_message(&SocketMessage::AnimationStyle(
|
||||
arg.style,
|
||||
arg.animation_type,
|
||||
))?;
|
||||
}
|
||||
|
||||
SubCommand::ResizeDelta(arg) => {
|
||||
@@ -2613,7 +2874,7 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
|
||||
file.write_all(content.as_bytes())?;
|
||||
|
||||
println!("Latest version of applications.yaml from https://github.com/LGUG2Z/komorebi-application-specific-configuration downloaded\n");
|
||||
println!("Latest version of applications.json from https://github.com/LGUG2Z/komorebi-application-specific-configuration downloaded\n");
|
||||
println!(
|
||||
"You can add this to your komorebi.json static configuration file like this: \n\n\"app_specific_configuration_path\": \"{}\"",
|
||||
output_file.display().to_string().replace("\\", "/")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user