mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-12 14:30:25 +01:00
Compare commits
163 Commits
monitor-pr
...
feature/aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1834f092d7 | ||
|
|
aa5a36989f | ||
|
|
b612066367 | ||
|
|
cbe5b24f73 | ||
|
|
2ead216eeb | ||
|
|
b0944662fa | ||
|
|
1376d7be04 | ||
|
|
5da72e10df | ||
|
|
d21ffb28cc | ||
|
|
4e27febdc0 | ||
|
|
3c5852ae20 | ||
|
|
7943fccb1b | ||
|
|
2d2cea31c0 | ||
|
|
a17924c2af | ||
|
|
91519227d4 | ||
|
|
f91d0aabf5 | ||
|
|
d5b6584042 | ||
|
|
6db317d425 | ||
|
|
07a1538905 | ||
|
|
853db2f15f | ||
|
|
c9f180ce0f | ||
|
|
bc00f54c90 | ||
|
|
c57759242a | ||
|
|
6f27de8e5c | ||
|
|
a9e98034b0 | ||
|
|
3489163793 | ||
|
|
71d65cf4a1 | ||
|
|
a5e6828d1e | ||
|
|
96605f72a7 | ||
|
|
18bb060b71 | ||
|
|
20f370a51d | ||
|
|
2d1613b4d9 | ||
|
|
216154b975 | ||
|
|
fe9b7e53b7 | ||
|
|
d9bffa06df | ||
|
|
d34363af8f | ||
|
|
db4b0dd63b | ||
|
|
7b563aac5e | ||
|
|
b7198242ff | ||
|
|
a1f1be0afe | ||
|
|
e4b7adeb0f | ||
|
|
818ac3404c | ||
|
|
d6ae81af13 | ||
|
|
109227b74c | ||
|
|
ddb600f745 | ||
|
|
167ec92811 | ||
|
|
d110e12a62 | ||
|
|
21bd09e419 | ||
|
|
01ccf70ad5 | ||
|
|
c3f135703e | ||
|
|
3720ce42d0 | ||
|
|
1080159e68 | ||
|
|
360d0915a1 | ||
|
|
182c1e6a96 | ||
|
|
14d2ebd756 | ||
|
|
50b89cc1df | ||
|
|
df409902bb | ||
|
|
df19d06333 | ||
|
|
96d094d9d7 | ||
|
|
2916256e79 | ||
|
|
21a2138330 | ||
|
|
6addfed1ce | ||
|
|
6ba0ba79f9 | ||
|
|
254fcc988f | ||
|
|
7005a01d9c | ||
|
|
de3d4d0d99 | ||
|
|
b69db863f1 | ||
|
|
286bb0070c | ||
|
|
45894be4ff | ||
|
|
bc67936dd3 | ||
|
|
c06d9afa3b | ||
|
|
b799fd3077 | ||
|
|
3c03528750 | ||
|
|
821a124771 | ||
|
|
8f7b9202b2 | ||
|
|
13e2cbc7a1 | ||
|
|
ff653e78af | ||
|
|
6d038b8b18 | ||
|
|
45a5941872 | ||
|
|
f54097f094 | ||
|
|
29b14f8dc8 | ||
|
|
a1cf5ba29c | ||
|
|
a60e5a77c2 | ||
|
|
f722905be1 | ||
|
|
b5eafc6b96 | ||
|
|
c367967301 | ||
|
|
780635c8ef | ||
|
|
d5bec7afa4 | ||
|
|
974aa0d000 | ||
|
|
0f9c23b6f4 | ||
|
|
6ea71834a1 | ||
|
|
81451cb17a | ||
|
|
7653495e31 | ||
|
|
0cdce8fc2a | ||
|
|
67f14730d0 | ||
|
|
ef9e734680 | ||
|
|
faa7786979 | ||
|
|
3c8a6cb7bd | ||
|
|
50a279239a | ||
|
|
2c8f25ef82 | ||
|
|
bdc1cad597 | ||
|
|
e2f2d6b919 | ||
|
|
aebe8792ac | ||
|
|
9fb6f8ebcd | ||
|
|
6eb6129618 | ||
|
|
5b997b6ea6 | ||
|
|
9414466646 | ||
|
|
9e87baa8b8 | ||
|
|
2a67c9c786 | ||
|
|
60bc96e9a5 | ||
|
|
5e9d573f0b | ||
|
|
128db85054 | ||
|
|
cc7dbde049 | ||
|
|
a5735c4186 | ||
|
|
48cb3db2fe | ||
|
|
24bff7e527 | ||
|
|
b985659e30 | ||
|
|
1d0ac9b555 | ||
|
|
7b1ece9680 | ||
|
|
00fc5382f6 | ||
|
|
9e37baa88a | ||
|
|
9a65a4ae92 | ||
|
|
a511cbd263 | ||
|
|
83cc7bf7c0 | ||
|
|
8bf4ab9f15 | ||
|
|
31864b1570 | ||
|
|
c022438a37 | ||
|
|
3d518f73ca | ||
|
|
d2d6484e38 | ||
|
|
67a3c3546f | ||
|
|
888b674646 | ||
|
|
5abab46290 | ||
|
|
ad8375eebe | ||
|
|
a11da2167c | ||
|
|
6b9a0843fd | ||
|
|
41732e2f5f | ||
|
|
9a58c1ee42 | ||
|
|
edc87d9940 | ||
|
|
133311bbe2 | ||
|
|
280aebf15d | ||
|
|
a488890a04 | ||
|
|
9a0ee8e8dd | ||
|
|
f23510055a | ||
|
|
a5fb5527c6 | ||
|
|
3f6e19b8b4 | ||
|
|
aa24c41967 | ||
|
|
1320b7440e | ||
|
|
cad2eb9a63 | ||
|
|
b7a987be8f | ||
|
|
e8f6a66bed | ||
|
|
fd97c7230d | ||
|
|
cc60f55cec | ||
|
|
270374497c | ||
|
|
6c90001c00 | ||
|
|
3232d9242a | ||
|
|
e57b08d073 | ||
|
|
cfb0c7f2ce | ||
|
|
5cff90a62b | ||
|
|
da7a9394d8 | ||
|
|
88684f991f | ||
|
|
03fdbea5cd | ||
|
|
340c137342 | ||
|
|
e46a0757e3 |
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,52 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]: Short descriptive title"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See bug
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots and Videos**
|
||||
Add screenshots and videos to help explain your problem.
|
||||
|
||||
**Operating System**
|
||||
Provide the output of `systeminfo | grep "^OS Name\|^OS Version"`
|
||||
|
||||
For example:
|
||||
```
|
||||
OS Name: Microsoft Windows 11 Pro
|
||||
OS Version: 10.0.22000 N/A Build 22000
|
||||
```
|
||||
|
||||
**`komorebic check` Output**
|
||||
Provide the output of `komorebic check`
|
||||
|
||||
For example:
|
||||
```
|
||||
No KOMOREBI_CONFIG_HOME detected, defaulting to C:\Users\LGUG2Z
|
||||
|
||||
Looking for configuration files in C:\Users\LGUG2Z
|
||||
|
||||
No komorebi configuration found in C:\Users\LGUG2Z
|
||||
|
||||
If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration
|
||||
```
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
In particular, if you have any other AHK scripts or software running that handle any aspect of window management or manipulation
|
||||
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
labels: [bug]
|
||||
title: "[BUG]: "
|
||||
body:
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
description: >
|
||||
Please provide a short summary of the bug, along with any information
|
||||
you feel is relevant to replicating the bug.
|
||||
|
||||
You may include screenshots and videos in this section.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Version Information
|
||||
description: >
|
||||
Please provide information about the versions of Windows and komorebi
|
||||
running on your machine.
|
||||
|
||||
Do not submit a bug if you are not using an official version of Windows
|
||||
such as AtlasOS; only official versions of Windows are supported.
|
||||
|
||||
```
|
||||
systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version"
|
||||
```
|
||||
|
||||
```
|
||||
komorebic --version
|
||||
```
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Komorebi Configuration
|
||||
description: >
|
||||
Please provide your configuration file (komorebi.json or komorebi.bar.json)
|
||||
render: json
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Hotkey Configuration
|
||||
description: >
|
||||
Please provide your whkdrc or komorebi.ahk hotkey configuration file
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Output of komorebic check
|
||||
description: >
|
||||
Please provide the output of `komorebic check`
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Komorebi Documentation
|
||||
url: https://lgug2z.github.io/komorebi/
|
||||
about: Please search the documentation website before opening an issue
|
||||
- name: Komorebi Quickstart Tutorial Video
|
||||
url: https://www.youtube.com/watch?v=MMZUAtHbTYY
|
||||
about: If you are new, please make sure you watch the quickstart tutorial video
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEAT]: Short descriptive title"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
37
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
37
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Feature request
|
||||
description: Suggest a new feature
|
||||
labels: [enhancement]
|
||||
title: "[FEAT]: "
|
||||
body:
|
||||
- type: dropdown
|
||||
id: Sponsors
|
||||
attributes:
|
||||
label: Sponsorship Information
|
||||
description: >
|
||||
Feature requests are considered from individuals who are $5+ monthly sponsors to the project.
|
||||
|
||||
Please specify the platform you use to sponsor the project.
|
||||
options:
|
||||
- GitHub Sponsors
|
||||
- Ko-fi
|
||||
- Discord
|
||||
- YouTube
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Suggestion
|
||||
description: >
|
||||
Please share your suggestion here. Be sure to include all necessary context.
|
||||
|
||||
If you sponsor on a platform where you use a different username, please specify the username here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: >
|
||||
Please share share alternatives you have considered here.
|
||||
249
.github/workflows/windows.yaml
vendored
249
.github/workflows/windows.yaml
vendored
@@ -14,125 +14,184 @@ on:
|
||||
tags:
|
||||
- v*
|
||||
schedule:
|
||||
- cron: "30 0 * * 1" # Every Monday at half past midnight UTC
|
||||
- cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static
|
||||
strategy:
|
||||
fail-fast: false
|
||||
fail-fast: true
|
||||
matrix:
|
||||
target:
|
||||
- x86_64-pc-windows-msvc
|
||||
platform:
|
||||
- os-name: Windows-x86_64
|
||||
runs-on: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- os-name: Windows-aarch64
|
||||
runs-on: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.platform.runs-on }}
|
||||
permissions: write-all
|
||||
env:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Prep cargo dirs
|
||||
run: |
|
||||
New-Item "${env:USERPROFILE}\.cargo\registry" -ItemType Directory -Force
|
||||
New-Item "${env:USERPROFILE}\.cargo\git" -ItemType Directory -Force
|
||||
shell: powershell
|
||||
- name: Set environment variables appropriately for the build
|
||||
run: |
|
||||
echo "%USERPROFILE%\.cargo\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
|
||||
echo "TARGET=${{ matrix.target }}" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
||||
echo "SKIP_TESTS=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
||||
- name: Cache cargo registry, git trees and binaries
|
||||
uses: actions/cache@v4
|
||||
- run: rustup toolchain install stable --profile minimal
|
||||
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Get rustc commit hash
|
||||
id: cargo-target-cache
|
||||
run: |
|
||||
echo "::set-output name=rust_hash::$(rustc -Vv | grep commit-hash | awk '{print $2}')"
|
||||
shell: bash
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v4
|
||||
cache-on-failure: "true"
|
||||
cache-all-crates: "true"
|
||||
- run: cargo +nightly fmt --check
|
||||
- run: cargo clippy
|
||||
- uses: houseabsolute/actions-rust-cross@v0
|
||||
with:
|
||||
path: target
|
||||
key: ${{ github.base_ref }}-${{ github.head_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ github.base_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Install Rustup using win.rustup.rs
|
||||
run: |
|
||||
# Disable the download progress bar which can cause perf issues
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
|
||||
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --profile=minimal
|
||||
shell: powershell
|
||||
- name: Ensure stable toolchain is up to date
|
||||
run: rustup update stable
|
||||
shell: bash
|
||||
- name: Install the target
|
||||
run: |
|
||||
rustup target install ${{ matrix.target }}
|
||||
- name: Run Cargo checks
|
||||
run: |
|
||||
cargo fmt --check
|
||||
cargo check
|
||||
cargo clippy
|
||||
- name: Run a full build
|
||||
run: |
|
||||
cargo build --locked --release --target ${{ matrix.target }}
|
||||
- name: Create MSI installer
|
||||
run: |
|
||||
command: "build"
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: "--locked --release"
|
||||
- run: |
|
||||
cargo install cargo-wix
|
||||
cargo wix -p komorebi --nocapture -I .\wix\main.wxs --target x86_64-pc-windows-msvc
|
||||
- name: Upload the built artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komorebi-${{ matrix.target }}
|
||||
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
|
||||
path: |
|
||||
target/${{ matrix.target }}/release/komorebi.exe
|
||||
target/${{ matrix.target }}/release/komorebic.exe
|
||||
target/${{ matrix.target }}/release/komorebic-no-console.exe
|
||||
target/${{ matrix.target }}/release/komorebi-gui.exe
|
||||
target/${{ matrix.target }}/release/komorebi.pdb
|
||||
target/${{ matrix.target }}/release/komorebic.pdb
|
||||
target/${{ matrix.target }}/release/komorebi-gui.pdb
|
||||
target/${{ matrix.platform.target }}/release/*.exe
|
||||
target/${{ matrix.platform.target }}/release/*.pdb
|
||||
target/wix/komorebi-*.msi
|
||||
retention-days: 7
|
||||
- name: Check GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: build --skip=validate --clean
|
||||
retention-days: 14
|
||||
|
||||
# Release
|
||||
- name: Generate changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
nightly:
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
permissions: write-all
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
|
||||
|
||||
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
|
||||
- shell: bash
|
||||
run: |
|
||||
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
|
||||
git tag -d nightly || true
|
||||
git tag nightly
|
||||
kokai release --no-emoji --add-links github:commits,issues --ref nightly >"CHANGELOG.md"
|
||||
- shell: bash
|
||||
run: |
|
||||
gh release delete nightly --yes || true
|
||||
git push origin :nightly || true
|
||||
gh release create nightly \
|
||||
--target $GITHUB_SHA \
|
||||
--prerelease \
|
||||
--title "komorebi nightly (${GITHUB_SHA})" \
|
||||
--notes-file CHANGELOG.md
|
||||
komorebi-nightly-x86_64-pc-windows-msvc.zip \
|
||||
komorebi-nightly-x86_64.msi \
|
||||
komorebi-nightly-aarch64-pc-windows-msvc.zip \
|
||||
komorebi-nightly-aarch64.msi \
|
||||
checksums.txt
|
||||
|
||||
release-dry-run:
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
permissions: write-all
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
|
||||
|
||||
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
|
||||
git tag -d nightly || true
|
||||
kokai release --no-emoji --add-links github:commits,issues --ref "${{ github.ref_name }}" >"CHANGELOG.md"
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
body_path: "CHANGELOG.md"
|
||||
files: |
|
||||
checksums.txt
|
||||
*.zip
|
||||
*.msi
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
permissions: write-all
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
|
||||
|
||||
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
|
||||
git tag -d nightly || true
|
||||
kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md"
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --skip=validate --clean --release-notes=CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
|
||||
- name: Add MSI to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
files: "target/wix/komorebi-*.msi"
|
||||
body_path: "CHANGELOG.md"
|
||||
files: |
|
||||
checksums.txt
|
||||
*.zip
|
||||
*.msi
|
||||
|
||||
winget:
|
||||
name: Publish to WinGet
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
needs: release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,5 +3,5 @@
|
||||
/target
|
||||
CHANGELOG.md
|
||||
dummy.go
|
||||
komorebi.ahk
|
||||
komorebic/applications.yaml
|
||||
komorebic/applications.yaml
|
||||
/.vs
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# Adapted from https://jondot.medium.com/shipping-rust-binaries-with-goreleaser-d5aa42a46be0
|
||||
project_name: komorebi
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- powershell.exe -Command "New-Item -Path . -Name dummy.go -ItemType file -Force"
|
||||
- powershell.exe -Command "Add-Content -Path .\dummy.go -Value 'package main'"
|
||||
- powershell.exe -Command "Add-Content -Path .\dummy.go -Value 'func main() {}'"
|
||||
|
||||
builds:
|
||||
- id: komorebi
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebi
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi.exe" ".\dist\komorebi_windows_amd64_v1\komorebi.exe"
|
||||
- id: komorebic
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebic
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64_v1\komorebic.exe"
|
||||
- id: komorebic-no-console
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebic-no-console
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic-no-console.exe" ".\dist\komorebic-no-console_windows_amd64_v1\komorebic-no-console.exe"
|
||||
- id: komorebi-gui
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebi-gui
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-gui.exe" ".\dist\komorebi-gui_windows_amd64_v1\komorebi-gui.exe"
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE.md
|
||||
- CHANGELOG.md
|
||||
|
||||
checksum:
|
||||
name_template: checksums.txt
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
3399
Cargo.lock
generated
3399
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
41
Cargo.toml
41
Cargo.toml
@@ -2,28 +2,46 @@
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"derive-ahk",
|
||||
"komorebi",
|
||||
"komorebi-client",
|
||||
"komorebi-core",
|
||||
"komorebi-gui",
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes"
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
windows-interface = { version = "0.53" }
|
||||
windows-implement = { version = "0.53" }
|
||||
dunce = "1"
|
||||
dirs = "5"
|
||||
clap = { version = "4", features = ["derive", "wrap_help"] }
|
||||
chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
serde_json = { package = "serde_json_lenient", version = "0.1" }
|
||||
sysinfo = "0.30"
|
||||
eframe = "0.29"
|
||||
egui_extras = "0.29"
|
||||
dirs = "5"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
schemars = "0.8"
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
||||
serde_yaml = "0.9"
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
paste = "1"
|
||||
sysinfo = "0.31"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
|
||||
windows-implement = { version = "0.58" }
|
||||
windows-interface = { version = "0.58" }
|
||||
windows-core = { version = "0.58" }
|
||||
shadow-rs = "0.35"
|
||||
which = "6"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.54"
|
||||
version = "0.58"
|
||||
features = [
|
||||
"implement",
|
||||
"Win32_System_Com",
|
||||
@@ -40,5 +58,8 @@ features = [
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_SystemServices"
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control"
|
||||
]
|
||||
|
||||
27
LICENSE.md
27
LICENSE.md
@@ -1,6 +1,6 @@
|
||||
# PolyForm Strict License 1.0.0
|
||||
# Komorebi License
|
||||
|
||||
<https://polyformproject.org/licenses/strict/1.0.0>
|
||||
Version 1.0.0
|
||||
|
||||
## Acceptance
|
||||
|
||||
@@ -13,8 +13,14 @@ your licenses.
|
||||
The licensor grants you a copyright license for the software
|
||||
to do everything you might do with the software that would
|
||||
otherwise infringe the licensor's copyright in it for any
|
||||
permitted purpose, other than distributing the software or
|
||||
making changes or new works based on the software.
|
||||
permitted purpose. However, you may only make changes according
|
||||
to the [Changes License](#changes-license), and you may not
|
||||
distribute the software or new works based on the software.
|
||||
|
||||
## Changes License
|
||||
|
||||
The licensor grants you an additional copyright license to
|
||||
make changes for any permitted purpose.
|
||||
|
||||
## Patent License
|
||||
|
||||
@@ -22,10 +28,6 @@ The licensor grants you a patent license for the software that
|
||||
covers patent claims the licensor can license, or becomes able
|
||||
to license, that you would infringe by using the software.
|
||||
|
||||
## Noncommercial Purposes
|
||||
|
||||
Any noncommercial purpose is a permitted purpose.
|
||||
|
||||
## Personal Uses
|
||||
|
||||
Personal use for research, experiment, and testing for
|
||||
@@ -34,15 +36,6 @@ entertainment, hobby projects, amateur pursuits, or religious
|
||||
observance, without any anticipated commercial application,
|
||||
is use for a permitted purpose.
|
||||
|
||||
## Noncommercial Organizations
|
||||
|
||||
Use by any charitable organization, educational institution,
|
||||
public research organization, public safety or health
|
||||
organization, environmental protection organization,
|
||||
or government institution is use for a permitted purpose
|
||||
regardless of the source of funding or obligations resulting
|
||||
from the funding.
|
||||
|
||||
## Fair Use
|
||||
|
||||
You may have "fair use" rights for the software under the
|
||||
|
||||
8
PRIVACY.md
Normal file
8
PRIVACY.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Privacy Policy for Komorebi
|
||||
|
||||
No data about your device(s) or _komorebi_ usage leave your device.
|
||||
|
||||
## Data Maintained by Komorebi
|
||||
|
||||
_komorebi_ writes log files to and keeps a list of temporary window handles (HWNDs) currently managed by the process in
|
||||
the `$Env:LOCALAPPDATA\komorebi\` directory. This directory is owned by the user running the process.
|
||||
20
README.md
20
README.md
@@ -60,7 +60,7 @@ channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
|
||||
_komorebi_ development videos. If you would like to be notified of upcoming
|
||||
videos please subscribe and turn on notifications.
|
||||
|
||||
_komorebi_ is a free and open-source project, and one that encourages you to
|
||||
_komorebi_ is a free and source-available project, and one that encourages you to
|
||||
make charitable donations if you find the software to be useful and have the
|
||||
financial means.
|
||||
|
||||
@@ -82,7 +82,7 @@ A [detailed installation and quickstart
|
||||
guide](https://lgug2z.github.io/komorebi/installation.html) is available which shows how to get started
|
||||
using `scoop`, `winget` or building from source.
|
||||
|
||||
[](https://www.youtube.com/watch?v=H9-_c1egQ4g)
|
||||
[](https://www.youtube.com/watch?v=MMZUAtHbTYY)
|
||||
|
||||
# Comparison With Fancy Zones
|
||||
|
||||
@@ -99,6 +99,13 @@ video will answer the majority of your questions.
|
||||
|
||||
# Demonstrations
|
||||
|
||||
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
|
||||
unfocused window transparency and animations enabled, using a custom status bar integrated using
|
||||
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
|
||||
|
||||
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
|
||||
|
||||
|
||||
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
|
||||
11 with a terminal emulator, a web browser and a code editor. The original
|
||||
video can be viewed
|
||||
@@ -184,7 +191,8 @@ required.
|
||||
|
||||
## License
|
||||
|
||||
`komorebi` is licensed under the [PolyForm Strict 1.0.0
|
||||
`komorebi` is licensed under the [Komorebi 1.0.0 license](./LICENSE.md), which
|
||||
is a fork of the [PolyForm Strict 1.0.0
|
||||
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
|
||||
this means that you are free to do whatever you want with `komorebi` other than
|
||||
redistribution, or distribution of new works (ie. hard-forks) based on the
|
||||
@@ -272,7 +280,7 @@ If the named pipe exists, `komorebi` will start pushing JSON data of successfull
|
||||
|
||||
You may then filter on the `type` key to listen to the events that you are interested in. For a full list of possible
|
||||
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
|
||||
in `komorebi-core`.
|
||||
in `komorebi::core`.
|
||||
|
||||
Below is an example of how you can subscribe to and filter on events using a named pipe in `nodejs`.
|
||||
|
||||
@@ -351,7 +359,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.25"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.29"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
@@ -407,7 +415,7 @@ A TCP listener can optionally be exposed on a port of your choosing with the `--
|
||||
provided to `komorebi` or `komorebic start`, no TCP listener will be created.
|
||||
|
||||
Once created, your client may send
|
||||
any [SocketMessage](https://github.com/LGUG2Z/komorebi/blob/master/komorebi-core/src/lib.rs#L37) to `komorebi` in the
|
||||
any [SocketMessage](https://github.com/LGUG2Z/komorebi/blob/master/komorebi/src/core/mod.rs#L37) to `komorebi` in the
|
||||
same way that `komorebic` would.
|
||||
|
||||
This can be used if you would like to create your own alternative to `komorebic` which incorporates scripting and
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "derive-ahk"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
syn = "1.0"
|
||||
quote = "1.0"
|
||||
@@ -1,225 +0,0 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
#![no_implicit_prelude]
|
||||
|
||||
use ::std::clone::Clone;
|
||||
use ::std::convert::From;
|
||||
use ::std::convert::Into;
|
||||
use ::std::format;
|
||||
use ::std::iter::Extend;
|
||||
use ::std::iter::Iterator;
|
||||
use ::std::matches;
|
||||
use ::std::option::Option::Some;
|
||||
use ::std::string::String;
|
||||
use ::std::string::ToString;
|
||||
use ::std::unreachable;
|
||||
use ::std::vec::Vec;
|
||||
|
||||
use ::quote::quote;
|
||||
use ::syn::parse_macro_input;
|
||||
use ::syn::Data;
|
||||
use ::syn::DataEnum;
|
||||
use ::syn::DeriveInput;
|
||||
use ::syn::Fields;
|
||||
use ::syn::FieldsNamed;
|
||||
use ::syn::FieldsUnnamed;
|
||||
use ::syn::Meta;
|
||||
use ::syn::NestedMeta;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[proc_macro_derive(AhkFunction)]
|
||||
pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let name = input.ident;
|
||||
|
||||
match input.data {
|
||||
Data::Struct(s) => match s.fields {
|
||||
Fields::Named(FieldsNamed { named, .. }) => {
|
||||
let argument_idents = named
|
||||
.iter()
|
||||
// Filter out the flags
|
||||
.filter(|&f| {
|
||||
let mut include = true;
|
||||
for attribute in &f.attrs {
|
||||
if let ::std::result::Result::Ok(Meta::List(list)) =
|
||||
attribute.parse_meta()
|
||||
{
|
||||
for nested in list.nested {
|
||||
if let NestedMeta::Meta(Meta::Path(path)) = nested {
|
||||
if path.is_ident("long") {
|
||||
include = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
include
|
||||
})
|
||||
.map(|f| &f.ident);
|
||||
|
||||
let argument_idents_clone = argument_idents.clone();
|
||||
|
||||
let called_arguments = quote! {#(%#argument_idents_clone%) *}
|
||||
.to_string()
|
||||
.replace(" %", "%")
|
||||
.replace("% ", "%")
|
||||
.replace("%%", "% %");
|
||||
|
||||
let flag_idents = named
|
||||
.iter()
|
||||
// Filter only the flags
|
||||
.filter(|f| {
|
||||
let mut include = false;
|
||||
|
||||
for attribute in &f.attrs {
|
||||
if let ::std::result::Result::Ok(Meta::List(list)) =
|
||||
attribute.parse_meta()
|
||||
{
|
||||
for nested in list.nested {
|
||||
if let NestedMeta::Meta(Meta::Path(path)) = nested {
|
||||
// Identify them using the --long flag name
|
||||
if path.is_ident("long") {
|
||||
include = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
include
|
||||
})
|
||||
.map(|f| &f.ident);
|
||||
|
||||
let has_flags = flag_idents.clone().count() != 0;
|
||||
|
||||
if has_flags {
|
||||
let flag_idents_concat = flag_idents.clone();
|
||||
let argument_idents_concat = argument_idents.clone();
|
||||
|
||||
// Concat the args and flag args if there are flags
|
||||
let all_arguments =
|
||||
quote! {#(#argument_idents_concat,) * #(#flag_idents_concat), *}
|
||||
.to_string();
|
||||
|
||||
let flag_idents_clone = flag_idents.clone();
|
||||
let flags = quote! {#(--#flag_idents_clone) *}
|
||||
.to_string()
|
||||
.replace("- - ", "--")
|
||||
.replace('_', "-");
|
||||
|
||||
let called_flag_arguments = quote! {#(%#flag_idents%) *}
|
||||
.to_string()
|
||||
.replace(" %", "%")
|
||||
.replace("% ", "%")
|
||||
.replace("%%", "% %");
|
||||
|
||||
let flags_split: Vec<_> = flags.split(' ').collect();
|
||||
let flag_args_split: Vec<_> = called_flag_arguments.split(' ').collect();
|
||||
let mut consolidated_flags: Vec<String> = Vec::new();
|
||||
|
||||
for (idx, flag) in flags_split.iter().enumerate() {
|
||||
consolidated_flags.push(format!("{} {}", flag, flag_args_split[idx]));
|
||||
}
|
||||
|
||||
let all_flags = consolidated_flags.join(" ");
|
||||
|
||||
quote! {
|
||||
impl AhkFunction for #name {
|
||||
fn generate_ahk_function() -> String {
|
||||
::std::format!(r#"
|
||||
{}({}) {{
|
||||
RunWait, komorebic.exe {} {} {}, , Hide
|
||||
}}"#,
|
||||
::std::stringify!(#name),
|
||||
#all_arguments,
|
||||
::std::stringify!(#name).to_kebab_case(),
|
||||
#called_arguments,
|
||||
#all_flags,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let arguments = quote! {#(#argument_idents), *}.to_string();
|
||||
|
||||
quote! {
|
||||
impl AhkFunction for #name {
|
||||
fn generate_ahk_function() -> String {
|
||||
::std::format!(r#"
|
||||
{}({}) {{
|
||||
RunWait, komorebic.exe {} {}, , Hide
|
||||
}}"#,
|
||||
::std::stringify!(#name),
|
||||
#arguments,
|
||||
::std::stringify!(#name).to_kebab_case(),
|
||||
#called_arguments
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!("only to be used on structs with named fields"),
|
||||
},
|
||||
_ => unreachable!("only to be used on structs"),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(AhkLibrary)]
|
||||
pub fn ahk_library(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let name = input.ident;
|
||||
|
||||
match input.data {
|
||||
Data::Enum(DataEnum { variants, .. }) => {
|
||||
let enums = variants.iter().filter(|&v| {
|
||||
matches!(v.fields, Fields::Unit) || matches!(v.fields, Fields::Unnamed(..))
|
||||
});
|
||||
|
||||
let mut stream = ::proc_macro2::TokenStream::new();
|
||||
|
||||
for variant in enums.clone() {
|
||||
match &variant.fields {
|
||||
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
|
||||
for field in unnamed {
|
||||
stream.extend(quote! {
|
||||
v.push(#field::generate_ahk_function());
|
||||
});
|
||||
}
|
||||
}
|
||||
Fields::Unit => {
|
||||
let name = &variant.ident;
|
||||
stream.extend(quote! {
|
||||
v.push(::std::format!(r#"
|
||||
{}() {{
|
||||
RunWait, komorebic.exe {}, , Hide
|
||||
}}"#,
|
||||
::std::stringify!(#name),
|
||||
::std::stringify!(#name).to_kebab_case()
|
||||
));
|
||||
});
|
||||
}
|
||||
Fields::Named(_) => {
|
||||
unreachable!("only to be used with unnamed and unit fields");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
impl #name {
|
||||
fn generate_ahk_library() -> String {
|
||||
let mut v: Vec<String> = vec![String::from("; Generated by komorebic.exe")];
|
||||
|
||||
#stream
|
||||
|
||||
v.join("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!("only to be used on enums"),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
# ahk-library
|
||||
|
||||
```
|
||||
Generate a library of AutoHotKey helper functions
|
||||
|
||||
Usage: komorebic.exe ahk-library
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/animation-duration.md
Normal file
16
docs/cli/animation-duration.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# animation-duration
|
||||
|
||||
```
|
||||
Set the duration for movement animations in ms
|
||||
|
||||
Usage: komorebic.exe animation-duration <DURATION>
|
||||
|
||||
Arguments:
|
||||
<DURATION>
|
||||
Desired animation durations in ms
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/animation-fps.md
Normal file
16
docs/cli/animation-fps.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# animation-fps
|
||||
|
||||
```
|
||||
Set the frames per second for movement animations
|
||||
|
||||
Usage: komorebic.exe animation-fps <FPS>
|
||||
|
||||
Arguments:
|
||||
<FPS>
|
||||
Desired animation frames per second
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
20
docs/cli/animation-style.md
Normal file
20
docs/cli/animation-style.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# animation-style
|
||||
|
||||
```
|
||||
Set the ease function for movement animations
|
||||
|
||||
Usage: komorebic.exe animation-style [OPTIONS]
|
||||
|
||||
Options:
|
||||
-s, --style <STYLE>
|
||||
Desired ease function for animation
|
||||
|
||||
[default: linear]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart,
|
||||
ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ,
|
||||
ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/animation.md
Normal file
16
docs/cli/animation.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# animation
|
||||
|
||||
```
|
||||
Enable or disable movement animations
|
||||
|
||||
Usage: komorebic.exe animation <BOOLEAN_STATE>
|
||||
|
||||
Arguments:
|
||||
<BOOLEAN_STATE>
|
||||
[possible values: enable, disable]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/bar-configuration.md
Normal file
12
docs/cli/bar-configuration.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# bar-configuration
|
||||
|
||||
```
|
||||
Show the path to komorebi.bar.json
|
||||
|
||||
Usage: komorebic.exe bar-configuration
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
20
docs/cli/border-implementation.md
Normal file
20
docs/cli/border-implementation.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# border-implementation
|
||||
|
||||
```
|
||||
Set the border implementation
|
||||
|
||||
Usage: komorebic.exe border-implementation <STYLE>
|
||||
|
||||
Arguments:
|
||||
<STYLE>
|
||||
Desired border implementation
|
||||
|
||||
Possible values:
|
||||
- komorebi: Use the adjustable komorebi border implementation
|
||||
- windows: Use the thin Windows accent border implementation
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
```
|
||||
21
docs/cli/border-style.md
Normal file
21
docs/cli/border-style.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# border-style
|
||||
|
||||
```
|
||||
Set the border style
|
||||
|
||||
Usage: komorebic.exe border-style <STYLE>
|
||||
|
||||
Arguments:
|
||||
<STYLE>
|
||||
Desired border style
|
||||
|
||||
Possible values:
|
||||
- system: Use the system border style
|
||||
- rounded: Use the Windows 11-style rounded borders
|
||||
- square: Use the Windows 10-style square borders
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
```
|
||||
12
docs/cli/clear-all-workspace-rules.md
Normal file
12
docs/cli/clear-all-workspace-rules.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# clear-all-workspace-rules
|
||||
|
||||
```
|
||||
Remove all application association rules for all workspaces
|
||||
|
||||
Usage: komorebic.exe clear-all-workspace-rules
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/clear-named-workspace-rules.md
Normal file
16
docs/cli/clear-named-workspace-rules.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# clear-named-workspace-rules
|
||||
|
||||
```
|
||||
Remove all application association rules for a named workspace
|
||||
|
||||
Usage: komorebic.exe clear-named-workspace-rules <WORKSPACE>
|
||||
|
||||
Arguments:
|
||||
<WORKSPACE>
|
||||
Name of a workspace
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
19
docs/cli/clear-workspace-rules.md
Normal file
19
docs/cli/clear-workspace-rules.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# clear-workspace-rules
|
||||
|
||||
```
|
||||
Remove all application association rules for a workspace by monitor and workspace index
|
||||
|
||||
Usage: komorebic.exe clear-workspace-rules <MONITOR> <WORKSPACE>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# complete-configuration
|
||||
|
||||
```
|
||||
Signal that the final configuration option has been sent
|
||||
For legacy komorebi.ahk or komorebi.ps1 configurations, signal that the final configuration option has been sent
|
||||
|
||||
Usage: komorebic.exe complete-configuration
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ Options:
|
||||
--ahk
|
||||
Enable autostart of ahk
|
||||
|
||||
--bar
|
||||
Enable autostart of komorebi-bar
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
16
docs/cli/focus-stack-window.md
Normal file
16
docs/cli/focus-stack-window.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# focus-stack-window
|
||||
|
||||
```
|
||||
Focus the specified window index in the focused stack
|
||||
|
||||
Usage: komorebic.exe focus-stack-window <TARGET>
|
||||
|
||||
Arguments:
|
||||
<TARGET>
|
||||
Target index (zero-indexed)
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/monitor-information.md
Normal file
12
docs/cli/monitor-information.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# monitor-information
|
||||
|
||||
```
|
||||
Show information about connected monitors
|
||||
|
||||
Usage: komorebic.exe monitor-information
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# reload-configuration
|
||||
|
||||
```
|
||||
Reload ~/komorebi.ahk (if it exists)
|
||||
Reload legacy komorebi.ahk or komorebi.ps1 configurations (if they exist)
|
||||
|
||||
Usage: komorebic.exe reload-configuration
|
||||
|
||||
|
||||
16
docs/cli/replace-configuration.md
Normal file
16
docs/cli/replace-configuration.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# replace-configuration
|
||||
|
||||
```
|
||||
Replace the configuration of a running instance of komorebi from a static configuration file
|
||||
|
||||
Usage: komorebic.exe replace-configuration <PATH>
|
||||
|
||||
Arguments:
|
||||
<PATH>
|
||||
Static configuration JSON file from which the configuration should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/stack-all.md
Normal file
12
docs/cli/stack-all.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# stack-all
|
||||
|
||||
```
|
||||
Stack all windows on the focused workspace
|
||||
|
||||
Usage: komorebic.exe stack-all
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -24,6 +24,9 @@ Options:
|
||||
--ahk
|
||||
Start autohotkey configuration file
|
||||
|
||||
--bar
|
||||
Start komorebi-bar in a background process
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ Options:
|
||||
--whkd
|
||||
Stop whkd if it is running as a background process
|
||||
|
||||
--bar
|
||||
Stop komorebi-bar if it is running as a background process
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
|
||||
12
docs/cli/toggle-transparency.md
Normal file
12
docs/cli/toggle-transparency.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-transparency
|
||||
|
||||
```
|
||||
Toggle transparency for unfocused windows
|
||||
|
||||
Usage: komorebic.exe toggle-transparency
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/transparency-alpha.md
Normal file
16
docs/cli/transparency-alpha.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# transparency-alpha
|
||||
|
||||
```
|
||||
Set the alpha value for unfocused window transparency
|
||||
|
||||
Usage: komorebic.exe transparency-alpha <ALPHA>
|
||||
|
||||
Arguments:
|
||||
<ALPHA>
|
||||
Alpha
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/transparency.md
Normal file
16
docs/cli/transparency.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# transparency
|
||||
|
||||
```
|
||||
Enable or disable transparency for unfocused windows
|
||||
|
||||
Usage: komorebic.exe transparency <BOOLEAN_STATE>
|
||||
|
||||
Arguments:
|
||||
<BOOLEAN_STATE>
|
||||
[possible values: enable, disable]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/unstack-all.md
Normal file
12
docs/cli/unstack-all.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# unstack-all
|
||||
|
||||
```
|
||||
Unstack all windows in the focused container
|
||||
|
||||
Usage: komorebic.exe unstack-all
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# watch-configuration
|
||||
|
||||
```
|
||||
Enable or disable watching of ~/komorebi.ahk (if it exists)
|
||||
Enable or disable watching of legacy komorebi.ahk or komorebi.ps1 configurations (if they exist)
|
||||
|
||||
Usage: komorebic.exe watch-configuration <BOOLEAN_STATE>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Arguments:
|
||||
Possible values:
|
||||
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
|
||||
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
25
docs/common-workflows/animations.md
Normal file
25
docs/common-workflows/animations.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Animations
|
||||
|
||||
If you would like to add window movement animations, ensure the following options are
|
||||
defined in the `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"animation": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Window movement animations only apply to actions taking place within the same monitor
|
||||
workspace.
|
||||
|
||||
You can optionally set a custom duration in ms with `animation.duration` (default: `250`),
|
||||
a custom style with `animation.style` (default: `Linear`), and a custom FPS value with
|
||||
`animation.fps` (default: `60`).
|
||||
|
||||
It is important to note that higher `fps` and a longer `duration` settings will result
|
||||
in increased CPU usage.
|
||||
|
||||
This feature is not considered stable, and you may encounter visual artifacts
|
||||
from time to time.
|
||||
@@ -1,4 +1,4 @@
|
||||
# AutoHotKey
|
||||
# AutoHotkey
|
||||
|
||||
If you would like to use Autohotkey, please make sure you have AutoHotKey v2
|
||||
installed.
|
||||
@@ -10,8 +10,8 @@ able to craft their own configuration files.
|
||||
If you would like to try out AHK, here is a simple sample configuration which
|
||||
largely matches the `whkdrc` sample configuration.
|
||||
|
||||
```
|
||||
{% include "../komorebi.ahk" %}
|
||||
```autohotkey
|
||||
{% include "./komorebi.ahk.txt" %}
|
||||
```
|
||||
|
||||
By default, the `komorebi.ahk` file should be located in the `$Env:USERPROFILE`
|
||||
@@ -19,4 +19,4 @@ directory, however, if `$Env:KOMOREBI_CONFIG_HOME` is set, it should be located
|
||||
there.
|
||||
|
||||
Once the file is in place, you can stop komorebi and whkd by running `komorebic stop --whkd`,
|
||||
and then start komorebi with Autohotkey by running `komorebic start --ahk`.
|
||||
and then start komorebi with Autohotkey by running `komorebic start --ahk`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Dynamically Layout Switching
|
||||
# Dynamic Layout Switching
|
||||
|
||||
With `komorebi` it is possible to define rules to automatically change the
|
||||
layout on a specified workspace when a threshold of window containers is met.
|
||||
|
||||
17
docs/common-workflows/set-display-index.md
Normal file
17
docs/common-workflows/set-display-index.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Setting a Given Display to a Specific Index
|
||||
|
||||
If you would like `komorebi` to remember monitor index positions, you will need to set the `display_index_preferences`
|
||||
configuration option in the static configuration file.
|
||||
|
||||
Display IDs can be found using `komorebic monitor-information`.
|
||||
|
||||
Then, in `komorebi.json`, you simply need to specify the preferred index position for each display ID:
|
||||
|
||||
```json
|
||||
{
|
||||
"display_index_preferences": {
|
||||
"0": "DEL4310-5&1a6c0954&0&UID209155",
|
||||
"1": "<another-display_id>"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -16,16 +16,17 @@ the example files have been downloaded. For most new users this will be in the
|
||||
komorebic quickstart
|
||||
```
|
||||
|
||||
With the example configurations downloaded, you can now start `komorebi` and `whkd.
|
||||
With the example configurations downloaded, you can now start `komorebi`,
|
||||
`komorebi-bar` and `whkd`.
|
||||
|
||||
```powershell
|
||||
komorebic start --whkd
|
||||
komorebic start --whkd --bar
|
||||
```
|
||||
|
||||
## komorebi.json
|
||||
|
||||
The example window manager configuration sets some sane defaults and provides
|
||||
five preconfigured workspaces on the primary monitor each with a different
|
||||
seven preconfigured workspaces on the primary monitor each with a different
|
||||
layout.
|
||||
|
||||
```json
|
||||
@@ -213,3 +214,24 @@ reference.
|
||||
If you want to use one of those key codes, put them into lower case and remove
|
||||
the `VK_` prefix. For example, the keycode `VK_OEM_PLUS` becomes `oem_plus` in
|
||||
the sample configuration above.
|
||||
|
||||
## komorebi.bar.json
|
||||
|
||||
The example status bar configuration sets some sane defaults and provides
|
||||
a number of pre-configured widgets on the primary monitor.
|
||||
|
||||
```json
|
||||
{% include "./komorebi.bar.example.json" %}
|
||||
```
|
||||
|
||||
### Themes
|
||||
|
||||
Themes can be set in either `komorebi.json` or `komorebi.bar.json`. If set
|
||||
in `komorebi.json`, the theme will be applied to both komorebi's borders and
|
||||
stackbars as well as the status bar.
|
||||
|
||||
If set in `komorebi.bar.json`, the theme will only be applied to the status bar.
|
||||
|
||||
All [Catppuccin palette variants](https://catppuccin.com/)
|
||||
and [most Base16 palette variants](https://tinted-theming.github.io/base16-gallery/)
|
||||
are available as themes.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Getting started
|
||||
|
||||
`komorebi` is a tiling window manager for Windows that is comprised comprised
|
||||
of two main binaries, `komorebi.exe`, which contains the window manager itself,
|
||||
`komorebi` is a tiling window manager for Windows that is comprised of two
|
||||
main binaries, `komorebi.exe`, which contains the window manager itself,
|
||||
and `komorebic.exe`, which is the main way to send commands to the tiling
|
||||
window manager.
|
||||
|
||||
It is important to note that neither `komorebi.exe` or `komorebic.exe` handle
|
||||
It is important to note that neither `komorebi.exe` nor `komorebic.exe` handle
|
||||
key bindings, because `komorebi` is a tiling window manager and not a hotkey
|
||||
daemon.
|
||||
|
||||
@@ -23,11 +23,15 @@ suggest that once you are familiar with the main `komorebic.exe` commands used
|
||||
to manipulate the window manager, you use
|
||||
[AutoHotKey](https://www.autohotkey.com/) to handle your key bindings.
|
||||
|
||||
`komorebi` also includes `komorebi-bar.exe`, a simple and reliable status bar which
|
||||
is deeply integrated with the tiling window manager, and can be customized with
|
||||
various widgets and themes.
|
||||
|
||||
## Installation
|
||||
|
||||
`komorebi` is available pre-built to install via
|
||||
[Scoop](https://scoop.sh/#/apps?q=komorebi) and
|
||||
[WinGet](https://winget.run/pkg/LGUG2Z/komorebi), and you may also built
|
||||
[WinGet](https://winget.run/pkg/LGUG2Z/komorebi), and you may also build
|
||||
it from [source](https://github.com/LGUG2Z/komorebi) if you would prefer.
|
||||
|
||||
- [Scoop](#scoop)
|
||||
@@ -37,7 +41,7 @@ it from [source](https://github.com/LGUG2Z/komorebi) if you would prefer.
|
||||
|
||||
## Long path support
|
||||
|
||||
It highly recommended that you enable support for long paths in Windows by
|
||||
It is highly recommended that you enable support for long paths in Windows by
|
||||
running the following command in an Administrator Terminal before installing
|
||||
`komorebi`.
|
||||
|
||||
@@ -45,7 +49,7 @@ running the following command in an Administrator Terminal before installing
|
||||
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
|
||||
```
|
||||
|
||||
## Disabling Unnecessary System Animations
|
||||
## Disabling unnecessary system animations
|
||||
|
||||
It is highly recommended that you enable the "Turn off all unnecessary animations (when possible)" option in
|
||||
"Control Panel > Ease of Access > Ease of Access Centre / Make the computer easier to see" for the best performance with
|
||||
@@ -115,6 +119,7 @@ cargo +stable install --path komorebi --locked
|
||||
cargo +stable install --path komorebic --locked
|
||||
cargo +stable install --path komorebic-no-console --locked
|
||||
cargo +stable install --path komorebi-gui --locked
|
||||
cargo +stable install --path komorebi-bar --locked
|
||||
```
|
||||
|
||||
If the binaries have been built and added to your `$PATH` correctly, you should
|
||||
@@ -128,3 +133,21 @@ an offline machine to install.
|
||||
|
||||
Once installed, proceed to get the [example configurations](example-configurations.md) (none of the commands for
|
||||
first-time set up and running komorebi require an internet connection).
|
||||
|
||||
## Uninstallation
|
||||
|
||||
Before uninstalling, first run `komorebic stop --whkd --bar` to make sure that
|
||||
the `komorebi`, `komorebi-bar` and `whkd` processes have been stopped.
|
||||
|
||||
Then, depending on whether you installed with Scoop or WinGet, run `scoop
|
||||
uninstall komorebi whkd` or `winget uninstall LGUG2Z.komorebi LGUG2Z.whkd`.
|
||||
|
||||
Finally, you can run the following commands in a PowerShell prompt to clean up
|
||||
files created by the `quickstart` command and any other runtime files:
|
||||
|
||||
```powershell
|
||||
rm $Env:USERPROFILE\komorebi.json
|
||||
rm $Env:USERPROFILE\applications.yaml
|
||||
rm $Env:USERPROFILE\.config\whkdrc
|
||||
rm -r -Force $Env:LOCALAPPDATA\komorebi
|
||||
```
|
||||
|
||||
71
docs/komorebi.ahk.txt
Normal file
71
docs/komorebi.ahk.txt
Normal file
@@ -0,0 +1,71 @@
|
||||
#Requires AutoHotkey v2.0.2
|
||||
#SingleInstance Force
|
||||
|
||||
Komorebic(cmd) {
|
||||
RunWait(format("komorebic.exe {}", cmd), , "Hide")
|
||||
}
|
||||
|
||||
!q::Komorebic("close")
|
||||
!m::Komorebic("minimize")
|
||||
|
||||
; Focus windows
|
||||
!h::Komorebic("focus left")
|
||||
!j::Komorebic("focus down")
|
||||
!k::Komorebic("focus up")
|
||||
!l::Komorebic("focus right")
|
||||
|
||||
!+[::Komorebic("cycle-focus previous")
|
||||
!+]::Komorebic("cycle-focus next")
|
||||
|
||||
; Move windows
|
||||
!+h::Komorebic("move left")
|
||||
!+j::Komorebic("move down")
|
||||
!+k::Komorebic("move up")
|
||||
!+l::Komorebic("move right")
|
||||
|
||||
; Stack windows
|
||||
!Left::Komorebic("stack left")
|
||||
!Down::Komorebic("stack down")
|
||||
!Up::Komorebic("stack up")
|
||||
!Right::Komorebic("stack right")
|
||||
!;::Komorebic("unstack")
|
||||
![::Komorebic("cycle-stack previous")
|
||||
!]::Komorebic("cycle-stack next")
|
||||
|
||||
; Resize
|
||||
!=::Komorebic("resize-axis horizontal increase")
|
||||
!-::Komorebic("resize-axis horizontal decrease")
|
||||
!+=::Komorebic("resize-axis vertical increase")
|
||||
!+_::Komorebic("resize-axis vertical decrease")
|
||||
|
||||
; Manipulate windows
|
||||
!t::Komorebic("toggle-float")
|
||||
!f::Komorebic("toggle-monocle")
|
||||
|
||||
; Window manager options
|
||||
!+r::Komorebic("retile")
|
||||
!p::Komorebic("toggle-pause")
|
||||
|
||||
; Layouts
|
||||
!x::Komorebic("flip-layout horizontal")
|
||||
!y::Komorebic("flip-layout vertical")
|
||||
|
||||
; Workspaces
|
||||
!1::Komorebic("focus-workspace 0")
|
||||
!2::Komorebic("focus-workspace 1")
|
||||
!3::Komorebic("focus-workspace 2")
|
||||
!4::Komorebic("focus-workspace 3")
|
||||
!5::Komorebic("focus-workspace 4")
|
||||
!6::Komorebic("focus-workspace 5")
|
||||
!7::Komorebic("focus-workspace 6")
|
||||
!8::Komorebic("focus-workspace 7")
|
||||
|
||||
; Move windows across workspaces
|
||||
!+1::Komorebic("move-to-workspace 0")
|
||||
!+2::Komorebic("move-to-workspace 1")
|
||||
!+3::Komorebic("move-to-workspace 2")
|
||||
!+4::Komorebic("move-to-workspace 3")
|
||||
!+5::Komorebic("move-to-workspace 4")
|
||||
!+6::Komorebic("move-to-workspace 5")
|
||||
!+7::Komorebic("move-to-workspace 6")
|
||||
!+8::Komorebic("move-to-workspace 7")
|
||||
76
docs/komorebi.bar.example.json
Normal file
76
docs/komorebi.bar.example.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.bar.json",
|
||||
"monitor": {
|
||||
"index": 0,
|
||||
"work_area_offset": {
|
||||
"left": 0,
|
||||
"top": 40,
|
||||
"right": 0,
|
||||
"bottom": 40
|
||||
}
|
||||
},
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
"palette": "Base16",
|
||||
"name": "Ashes",
|
||||
"accent": "Base0D"
|
||||
},
|
||||
"left_widgets": [
|
||||
{
|
||||
"Komorebi": {
|
||||
"workspaces": {
|
||||
"enable": true,
|
||||
"hide_empty_workspaces": false
|
||||
},
|
||||
"layout": {
|
||||
"enable": true
|
||||
},
|
||||
"focused_window": {
|
||||
"enable": true,
|
||||
"show_icon": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"right_widgets": [
|
||||
{
|
||||
"Media": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Storage": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Memory": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Network": {
|
||||
"enable": true,
|
||||
"show_total_data_transmitted": true,
|
||||
"show_network_activity": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Date": {
|
||||
"enable": true,
|
||||
"format": "DayDateMonthYear"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Time": {
|
||||
"enable": true,
|
||||
"format": "TwentyFourHour"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Battery": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.25/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
@@ -8,20 +8,17 @@
|
||||
"border": true,
|
||||
"border_width": 8,
|
||||
"border_offset": -1,
|
||||
"border_colours": {
|
||||
"single": "#42a5f5",
|
||||
"stack": "#00a542",
|
||||
"monocle": "#ff3399",
|
||||
"unfocused": "#808080"
|
||||
"theme": {
|
||||
"palette": "Base16",
|
||||
"name": "Ashes",
|
||||
"unfocused_border": "Base03",
|
||||
"bar_accent": "Base0D"
|
||||
},
|
||||
"stackbar": {
|
||||
"height": 40,
|
||||
"mode": "OnStack",
|
||||
"tabs": {
|
||||
"width": 300,
|
||||
"focused_text": "#00a542",
|
||||
"unfocused_text": "#b3b3b3",
|
||||
"background": "#141414"
|
||||
"width": 300
|
||||
}
|
||||
},
|
||||
"monitors": [
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# v0.1.22
|
||||
|
||||
In addition to the [changelog](https://github.com/LGUG2Z/komorebi/releases/tag/v0.1.22) of new features and fixes,
|
||||
please note the following changes from `v0.1.21` to adjust your configuration files accordingly.
|
||||
|
||||
@@ -49,8 +51,8 @@ A 1px border is drawn around the window edge. Users may see a gap for a single p
|
||||
transparent edge - this is the windows themed edge, and is not present for all applications.
|
||||
|
||||
```json
|
||||
{
|
||||
{
|
||||
"border_offset": 0,
|
||||
"border_width": 1
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
27
justfile
27
justfile
@@ -7,22 +7,24 @@ clean:
|
||||
fmt:
|
||||
cargo +nightly fmt
|
||||
cargo +stable clippy
|
||||
prettier --write README.md
|
||||
prettier --write .goreleaser.yml
|
||||
prettier --write .github/ISSUE_TEMPLATE/bug_report.yml
|
||||
prettier --write .github/ISSUE_TEMPLATE/config.yml
|
||||
prettier --write .github/ISSUE_TEMPLATE/feature_request.yml
|
||||
prettier --write .github/dependabot.yml
|
||||
prettier --write .github/FUNDING.yml
|
||||
prettier --write .github/workflows/windows.yaml
|
||||
|
||||
install-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
|
||||
|
||||
install-target target:
|
||||
cargo +stable install --path {{ target }} --locked
|
||||
|
||||
install:
|
||||
just install-target komorebic
|
||||
just install-target komorebic-no-console
|
||||
just install-target komorebi
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
run:
|
||||
cargo +stable run --bin komorebi --locked
|
||||
run target:
|
||||
cargo +stable run --bin {{ target }} --locked
|
||||
|
||||
warn $RUST_LOG="warn":
|
||||
just run
|
||||
@@ -40,9 +42,16 @@ deadlock $RUST_LOG="trace":
|
||||
cargo +stable run --bin komorebi --locked --features deadlock_detection
|
||||
|
||||
docgen:
|
||||
komorebic docgen
|
||||
cargo run --package komorebic -- docgen
|
||||
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
|
||||
|
||||
schemagen:
|
||||
komorebic static-config-schema > schema.json
|
||||
cargo run --package komorebic -- static-config-schema > schema.json
|
||||
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
|
||||
cargo run --package komorebi-bar -- --schema > schema.bar.json
|
||||
generate-schema-doc .\schema.json --config template_name=js_offline --config minify=false .\static-config-docs\
|
||||
|
||||
generate-schema-doc .\schema.bar.json --config template_name=js_offline --config minify=false .\bar-config-docs\
|
||||
|
||||
rm -Force .\bar-config-docs\schema.html
|
||||
mv .\bar-config-docs\schema.bar.html .\bar-config-docs\schema.html
|
||||
|
||||
37
komorebi-bar/Cargo.toml
Normal file
37
komorebi-bar/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui-phosphor = "0.7"
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
netdev = "0.31"
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
random_word = { version = "0.4", features = ["en"] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
starship-battery = "0.10"
|
||||
sysinfo = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
458
komorebi-bar/src/bar.rs
Normal file
458
komorebi-bar/src/bar.rs
Normal file
@@ -0,0 +1,458 @@
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiNotificationState;
|
||||
use crate::process_hwnd;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widget::WidgetConfig;
|
||||
use crate::BAR_HEIGHT;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_LEFT;
|
||||
use crate::MONITOR_RIGHT;
|
||||
use crate::MONITOR_TOP;
|
||||
use crossbeam_channel::Receiver;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::CentralPanel;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontData;
|
||||
use eframe::egui::FontDefinitions;
|
||||
use eframe::egui::FontFamily;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Layout;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::TextStyle;
|
||||
use font_loader::system_fonts;
|
||||
use font_loader::system_fonts::FontPropertyBuilder;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use komorebi_themes::Base16Value;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::CatppuccinValue;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Komobar {
|
||||
pub config: Arc<KomobarConfig>,
|
||||
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub rx_gui: Receiver<komorebi_client::Notification>,
|
||||
pub rx_config: Receiver<KomobarConfig>,
|
||||
pub bg_color: Rc<RefCell<Color32>>,
|
||||
pub scale_factor: f32,
|
||||
}
|
||||
|
||||
pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Color32>>) {
|
||||
match theme {
|
||||
KomobarTheme::Catppuccin {
|
||||
name: catppuccin,
|
||||
accent: catppuccin_value,
|
||||
} => match catppuccin {
|
||||
Catppuccin::Frappe => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::FRAPPE.base);
|
||||
}
|
||||
Catppuccin::Latte => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::LATTE.base);
|
||||
}
|
||||
Catppuccin::Macchiato => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::MACCHIATO.base);
|
||||
}
|
||||
Catppuccin::Mocha => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
|
||||
let catppuccin_value = catppuccin_value.unwrap_or_default();
|
||||
let accent = catppuccin_value.color32(catppuccin.as_theme());
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
style.visuals.override_text_color = None;
|
||||
});
|
||||
|
||||
bg_color.replace(catppuccin_egui::MOCHA.base);
|
||||
}
|
||||
},
|
||||
KomobarTheme::Base16 {
|
||||
name: base16,
|
||||
accent: base16_value,
|
||||
} => {
|
||||
ctx.set_style(base16.style());
|
||||
let base16_value = base16_value.unwrap_or_default();
|
||||
let accent = base16_value.color32(base16);
|
||||
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.selection.stroke.color = accent;
|
||||
style.visuals.widgets.hovered.fg_stroke.color = accent;
|
||||
style.visuals.widgets.active.fg_stroke.color = accent;
|
||||
});
|
||||
|
||||
bg_color.replace(base16.background());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Komobar {
|
||||
pub fn apply_config(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
config: &KomobarConfig,
|
||||
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
) {
|
||||
MAX_LABEL_WIDTH.store(
|
||||
config.max_label_width.unwrap_or(400.0) as i32,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
if let Some(font_family) = &config.font_family {
|
||||
tracing::info!("attempting to add custom font family: {font_family}");
|
||||
Self::add_custom_font(ctx, font_family);
|
||||
}
|
||||
|
||||
let position = config.position.clone().unwrap_or(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
|
||||
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
|
||||
}),
|
||||
end: Some(Position {
|
||||
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
|
||||
y: BAR_HEIGHT,
|
||||
}),
|
||||
});
|
||||
|
||||
if let Some(hwnd) = process_hwnd() {
|
||||
let start = position.start.unwrap_or(Position {
|
||||
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
|
||||
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
|
||||
});
|
||||
|
||||
let end = position.end.unwrap_or(Position {
|
||||
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
|
||||
y: BAR_HEIGHT,
|
||||
});
|
||||
|
||||
let rect = komorebi_client::Rect {
|
||||
left: start.x as i32,
|
||||
top: start.y as i32,
|
||||
right: end.x as i32,
|
||||
bottom: end.y as i32,
|
||||
};
|
||||
|
||||
let window = komorebi_client::Window::from(hwnd);
|
||||
match window.set_position(&rect, false) {
|
||||
Ok(_) => {
|
||||
tracing::info!("updated bar position");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match config.theme {
|
||||
Some(theme) => {
|
||||
apply_theme(ctx, theme, self.bg_color.clone());
|
||||
}
|
||||
None => {
|
||||
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|
||||
|_| dirs::home_dir().expect("there is no home directory"),
|
||||
|home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let config = home_dir.join("komorebi.json");
|
||||
match komorebi_client::StaticConfig::read(&config) {
|
||||
Ok(config) => {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(ctx, KomobarTheme::from(theme), self.bg_color.clone());
|
||||
|
||||
let stack_accent = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name, stack_border, ..
|
||||
} => stack_border
|
||||
.unwrap_or(CatppuccinValue::Green)
|
||||
.color32(name.as_theme()),
|
||||
KomorebiTheme::Base16 {
|
||||
name, stack_border, ..
|
||||
} => stack_border.unwrap_or(Base16Value::Base0B).color32(name),
|
||||
};
|
||||
|
||||
if let Some(state) = &self.komorebi_notification_state {
|
||||
state.borrow_mut().stack_accent = Some(stack_accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
ctx.set_style(Style::default());
|
||||
self.bg_color.replace(Style::default().visuals.panel_fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(font_size) = &config.font_size {
|
||||
tracing::info!("attempting to set custom font size: {font_size}");
|
||||
Self::set_font_size(ctx, *font_size);
|
||||
}
|
||||
|
||||
let mut komorebi_widget = None;
|
||||
let mut komorebi_widget_idx = None;
|
||||
let mut komorebi_notification_state = previous_notification_state;
|
||||
let mut side = None;
|
||||
|
||||
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
|
||||
if let WidgetConfig::Komorebi(config) = widget_config {
|
||||
komorebi_widget = Some(Komorebi::from(config));
|
||||
komorebi_widget_idx = Some(idx);
|
||||
side = Some(Side::Left);
|
||||
}
|
||||
}
|
||||
|
||||
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
|
||||
if let WidgetConfig::Komorebi(config) = widget_config {
|
||||
komorebi_widget = Some(Komorebi::from(config));
|
||||
komorebi_widget_idx = Some(idx);
|
||||
side = Some(Side::Right);
|
||||
}
|
||||
}
|
||||
|
||||
let mut left_widgets = config
|
||||
.left_widgets
|
||||
.iter()
|
||||
.map(|config| config.as_boxed_bar_widget())
|
||||
.collect::<Vec<Box<dyn BarWidget>>>();
|
||||
|
||||
let mut right_widgets = config
|
||||
.right_widgets
|
||||
.iter()
|
||||
.map(|config| config.as_boxed_bar_widget())
|
||||
.collect::<Vec<Box<dyn BarWidget>>>();
|
||||
|
||||
if let (Some(idx), Some(mut widget), Some(side)) =
|
||||
(komorebi_widget_idx, komorebi_widget, side)
|
||||
{
|
||||
match komorebi_notification_state {
|
||||
None => {
|
||||
komorebi_notification_state = Some(widget.komorebi_notification_state.clone());
|
||||
}
|
||||
Some(ref previous) => {
|
||||
previous
|
||||
.borrow_mut()
|
||||
.update_from_config(&widget.komorebi_notification_state.borrow());
|
||||
|
||||
widget.komorebi_notification_state = previous.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let boxed: Box<dyn BarWidget> = Box::new(widget);
|
||||
match side {
|
||||
Side::Left => left_widgets[idx] = boxed,
|
||||
Side::Right => right_widgets[idx] = boxed,
|
||||
}
|
||||
}
|
||||
|
||||
right_widgets.reverse();
|
||||
|
||||
self.left_widgets = left_widgets;
|
||||
self.right_widgets = right_widgets;
|
||||
|
||||
tracing::info!("widget configuration options applied");
|
||||
|
||||
self.komorebi_notification_state = komorebi_notification_state;
|
||||
}
|
||||
pub fn new(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
rx_config: Receiver<KomobarConfig>,
|
||||
config: Arc<KomobarConfig>,
|
||||
) -> Self {
|
||||
let mut komobar = Self {
|
||||
config: config.clone(),
|
||||
komorebi_notification_state: None,
|
||||
left_widgets: vec![],
|
||||
right_widgets: vec![],
|
||||
rx_gui,
|
||||
rx_config,
|
||||
bg_color: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
|
||||
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
|
||||
};
|
||||
|
||||
komobar.apply_config(&cc.egui_ctx, &config, None);
|
||||
// needs a double apply the first time for some reason
|
||||
komobar.apply_config(&cc.egui_ctx, &config, None);
|
||||
|
||||
komobar
|
||||
}
|
||||
|
||||
fn set_font_size(ctx: &Context, font_size: f32) {
|
||||
ctx.style_mut(|style| {
|
||||
style.text_styles = [
|
||||
(TextStyle::Small, FontId::new(9.0, FontFamily::Proportional)),
|
||||
(
|
||||
TextStyle::Body,
|
||||
FontId::new(font_size, FontFamily::Proportional),
|
||||
),
|
||||
(
|
||||
TextStyle::Button,
|
||||
FontId::new(font_size, FontFamily::Proportional),
|
||||
),
|
||||
(
|
||||
TextStyle::Heading,
|
||||
FontId::new(18.0, FontFamily::Proportional),
|
||||
),
|
||||
(
|
||||
TextStyle::Monospace,
|
||||
FontId::new(font_size, FontFamily::Monospace),
|
||||
),
|
||||
]
|
||||
.into();
|
||||
});
|
||||
}
|
||||
|
||||
fn add_custom_font(ctx: &Context, name: &str) {
|
||||
let mut fonts = FontDefinitions::default();
|
||||
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
|
||||
|
||||
let property = FontPropertyBuilder::new().family(name).build();
|
||||
|
||||
if let Some((font, _)) = system_fonts::get(&property) {
|
||||
fonts
|
||||
.font_data
|
||||
.insert(name.to_owned(), FontData::from_owned(font));
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(FontFamily::Proportional)
|
||||
.or_default()
|
||||
.insert(0, name.to_owned());
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(FontFamily::Monospace)
|
||||
.or_default()
|
||||
.push(name.to_owned());
|
||||
|
||||
// Tell egui to use these fonts:
|
||||
ctx.set_fonts(fonts);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl eframe::App for Komobar {
|
||||
// TODO: I think this is needed for transparency??
|
||||
// fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
|
||||
// egui::Rgba::TRANSPARENT.to_array()
|
||||
// let mut background = Color32::from_gray(18).to_normalized_gamma_f32();
|
||||
// background[3] = 0.9;
|
||||
// background
|
||||
// }
|
||||
|
||||
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
|
||||
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
|
||||
self.apply_config(
|
||||
ctx,
|
||||
&self.config.clone(),
|
||||
self.komorebi_notification_state.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(updated_config) = self.rx_config.try_recv() {
|
||||
self.apply_config(
|
||||
ctx,
|
||||
&updated_config,
|
||||
self.komorebi_notification_state.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
|
||||
komorebi_notification_state
|
||||
.borrow_mut()
|
||||
.handle_notification(
|
||||
ctx,
|
||||
self.config.monitor.index,
|
||||
self.rx_gui.clone(),
|
||||
self.bg_color.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
let frame = if let Some(frame) = &self.config.frame {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::symmetric(
|
||||
frame.inner_margin.x,
|
||||
frame.inner_margin.y,
|
||||
))
|
||||
.fill(*self.bg_color.borrow())
|
||||
} else {
|
||||
Frame::none().fill(*self.bg_color.borrow())
|
||||
};
|
||||
|
||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
|
||||
for w in &mut self.left_widgets {
|
||||
w.render(ctx, ui);
|
||||
}
|
||||
});
|
||||
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
for w in &mut self.right_widgets {
|
||||
w.render(ctx, ui);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
160
komorebi-bar/src/battery.rs
Normal file
160
komorebi-bar/src/battery.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use starship_battery::units::ratio::percent;
|
||||
use starship_battery::Manager;
|
||||
use starship_battery::State;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct BatteryConfig {
|
||||
/// Enable the Battery widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<BatteryConfig> for Battery {
|
||||
fn from(value: BatteryConfig) -> Self {
|
||||
let manager = Manager::new().unwrap();
|
||||
let mut last_state = String::new();
|
||||
let mut state = None;
|
||||
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
|
||||
|
||||
if let Ok(mut batteries) = manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>();
|
||||
match first.state() {
|
||||
State::Charging => state = Some(BatteryState::Charging),
|
||||
State::Discharging => state = Some(BatteryState::Discharging),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
last_state = match prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
manager,
|
||||
last_state,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: prefix,
|
||||
state: state.unwrap_or(BatteryState::Discharging),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum BatteryState {
|
||||
Charging,
|
||||
Discharging,
|
||||
}
|
||||
|
||||
pub struct Battery {
|
||||
pub enable: bool,
|
||||
manager: Manager,
|
||||
pub state: BatteryState,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_state: String,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Battery {
|
||||
fn output(&mut self) -> String {
|
||||
let mut output = self.last_state.clone();
|
||||
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
output.clear();
|
||||
|
||||
if let Ok(mut batteries) = self.manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>();
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => self.state = BatteryState::Discharging,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
output = match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state.clone_from(&output);
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Battery {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let emoji = match self.state {
|
||||
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
|
||||
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
|
||||
};
|
||||
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
ui.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
178
komorebi-bar/src/config.rs
Normal file
178
komorebi-bar/src/config.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use crate::widget::WidgetConfig;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::Rect;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.30`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar positioning options
|
||||
#[serde(alias = "viewport")]
|
||||
pub position: Option<PositionConfig>,
|
||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)
|
||||
pub frame: Option<FrameConfig>,
|
||||
/// Monitor options
|
||||
pub monitor: MonitorConfig,
|
||||
/// Font family
|
||||
pub font_family: Option<String>,
|
||||
/// Font size (default: 12.5)
|
||||
pub font_size: Option<f32>,
|
||||
/// Max label width before text truncation (default: 400.0)
|
||||
pub max_label_width: Option<f32>,
|
||||
/// Theme
|
||||
pub theme: Option<KomobarTheme>,
|
||||
/// Left side widgets (ordered left-to-right)
|
||||
pub left_widgets: Vec<WidgetConfig>,
|
||||
/// Right side widgets (ordered left-to-right)
|
||||
pub right_widgets: Vec<WidgetConfig>,
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn aliases(raw: &str) {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("position", ["viewport"]);
|
||||
map.insert("end", ["inner_frame"]);
|
||||
|
||||
let mut display = false;
|
||||
|
||||
for aliases in map.values() {
|
||||
for a in aliases {
|
||||
if raw.contains(a) {
|
||||
display = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if display {
|
||||
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
|
||||
for (canonical, aliases) in map {
|
||||
for alias in aliases {
|
||||
if raw.contains(alias) {
|
||||
println!(r#""{alias}" is now "{canonical}""#);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PositionConfig {
|
||||
/// The desired starting position of the bar (0,0 = top left of the screen)
|
||||
#[serde(alias = "position")]
|
||||
pub start: Option<Position>,
|
||||
/// The desired size of the bar from the starting position (usually monitor width x desired height)
|
||||
#[serde(alias = "inner_size")]
|
||||
pub end: Option<Position>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FrameConfig {
|
||||
/// Margin inside the painted frame
|
||||
pub inner_margin: Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MonitorConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub index: usize,
|
||||
/// Automatically apply a work area offset for this monitor to accommodate the bar
|
||||
pub work_area_offset: Option<Rect>,
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let mut value: Self = match path.extension().unwrap().to_string_lossy().as_str() {
|
||||
"json" => serde_json::from_str(&content)?,
|
||||
_ => panic!("unsupported format"),
|
||||
};
|
||||
|
||||
if value.frame.is_none() {
|
||||
value.frame = Some(FrameConfig {
|
||||
inner_margin: Position { x: 10.0, y: 10.0 },
|
||||
});
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Position {
|
||||
/// X coordinate
|
||||
pub x: f32,
|
||||
/// Y coordinate
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl From<Position> for Vec2 {
|
||||
fn from(value: Position) -> Self {
|
||||
Self {
|
||||
x: value.x,
|
||||
y: value.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Position> for Pos2 {
|
||||
fn from(value: Position) -> Self {
|
||||
Self {
|
||||
x: value.x,
|
||||
y: value.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "palette")]
|
||||
pub enum KomobarTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
name: komorebi_themes::Catppuccin,
|
||||
accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
name: komorebi_themes::Base16,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<KomorebiTheme> for KomobarTheme {
|
||||
fn from(value: KomorebiTheme) -> Self {
|
||||
match value {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name, bar_accent, ..
|
||||
} => Self::Catppuccin {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
},
|
||||
KomorebiTheme::Base16 {
|
||||
name, bar_accent, ..
|
||||
} => Self::Base16 {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum LabelPrefix {
|
||||
/// Show no prefix
|
||||
None,
|
||||
/// Show an icon
|
||||
Icon,
|
||||
/// Show text
|
||||
Text,
|
||||
/// Show an icon and text
|
||||
IconAndText,
|
||||
}
|
||||
120
komorebi-bar/src/cpu.rs
Normal file
120
komorebi-bar/src/cpu.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::RefreshKind;
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CpuConfig {
|
||||
/// Enable the Cpu widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<CpuConfig> for Cpu {
|
||||
fn from(value: CpuConfig) -> Self {
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
|
||||
|
||||
system.refresh_cpu_usage();
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Cpu {
|
||||
pub enable: bool,
|
||||
system: System,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
fn output(&mut self) -> String {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.system.refresh_cpu_usage();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let used = self.system.global_cpu_usage();
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Cpu {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CPU.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
137
komorebi-bar/src/date.rs
Normal file
137
komorebi-bar/src/date.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DateConfig {
|
||||
/// Enable the Date widget
|
||||
pub enable: bool,
|
||||
/// Set the Date format
|
||||
pub format: DateFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<DateConfig> for Date {
|
||||
fn from(value: DateConfig) -> Self {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum DateFormat {
|
||||
/// Month/Date/Year format (09/08/24)
|
||||
MonthDateYear,
|
||||
/// Year-Month-Date format (2024-09-08)
|
||||
YearMonthDate,
|
||||
/// Date-Month-Year format (8-Sep-2024)
|
||||
DateMonthYear,
|
||||
/// Day Date Month Year format (8 September 2024)
|
||||
DayDateMonthYear,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl DateFormat {
|
||||
pub fn next(&mut self) {
|
||||
match self {
|
||||
DateFormat::MonthDateYear => *self = Self::YearMonthDate,
|
||||
DateFormat::YearMonthDate => *self = Self::DateMonthYear,
|
||||
DateFormat::DateMonthYear => *self = Self::DayDateMonthYear,
|
||||
DateFormat::DayDateMonthYear => *self = Self::MonthDateYear,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn fmt_string(&self) -> String {
|
||||
match self {
|
||||
DateFormat::MonthDateYear => String::from("%D"),
|
||||
DateFormat::YearMonthDate => String::from("%F"),
|
||||
DateFormat::DateMonthYear => String::from("%v"),
|
||||
DateFormat::DayDateMonthYear => String::from("%A %e %B %Y"),
|
||||
DateFormat::Custom(custom) => custom.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Date {
|
||||
pub enable: bool,
|
||||
pub format: DateFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
}
|
||||
|
||||
impl Date {
|
||||
fn output(&mut self) -> String {
|
||||
chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Date {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CALENDAR_DOTS.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.insert_str(0, "DATE: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.next()
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
546
komorebi-bar/src/komorebi.rs
Normal file
546
komorebi-bar/src/komorebi.rs
Normal file
@@ -0,0 +1,546 @@
|
||||
use crate::bar::apply_theme;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::WIDGET_SPACING;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ColorImage;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Image;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::SelectableLabel;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::TextureHandle;
|
||||
use eframe::egui::TextureOptions;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::CycleDirection;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiConfig {
|
||||
/// Configure the Workspaces widget
|
||||
pub workspaces: KomorebiWorkspacesConfig,
|
||||
/// Configure the Layout widget
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
/// Configure the Focused Window widget
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
/// Configure the Configuration Switcher widget
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiWorkspacesConfig {
|
||||
/// Enable the Komorebi Workspaces widget
|
||||
pub enable: bool,
|
||||
/// Hide workspaces without any windows
|
||||
pub hide_empty_workspaces: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiLayoutConfig {
|
||||
/// Enable the Komorebi Layout widget
|
||||
pub enable: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiFocusedWindowConfig {
|
||||
/// Enable the Komorebi Focused Window widget
|
||||
pub enable: bool,
|
||||
/// Show the icon of the currently focused window
|
||||
pub show_icon: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiConfigurationSwitcherConfig {
|
||||
/// Enable the Komorebi Configurations widget
|
||||
pub enable: bool,
|
||||
/// A map of display friendly name => path to configuration.json
|
||||
pub configurations: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl From<&KomorebiConfig> for Komorebi {
|
||||
fn from(value: &KomorebiConfig) -> Self {
|
||||
let configuration_switcher =
|
||||
if let Some(configuration_switcher) = &value.configuration_switcher {
|
||||
let mut configuration_switcher = configuration_switcher.clone();
|
||||
for (_, location) in configuration_switcher.configurations.iter_mut() {
|
||||
*location = dunce::simplified(&PathBuf::from(location.clone()))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
Some(configuration_switcher)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState {
|
||||
selected_workspace: String::new(),
|
||||
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
workspaces: vec![],
|
||||
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
|
||||
mouse_follows_focus: true,
|
||||
work_area_offset: None,
|
||||
focused_container_information: (vec![], vec![], 0),
|
||||
stack_accent: None,
|
||||
})),
|
||||
workspaces: value.workspaces,
|
||||
layout: value.layout,
|
||||
focused_window: value.focused_window,
|
||||
configuration_switcher,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Komorebi {
|
||||
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
|
||||
pub workspaces: KomorebiWorkspacesConfig,
|
||||
pub layout: Option<KomorebiLayoutConfig>,
|
||||
pub focused_window: Option<KomorebiFocusedWindowConfig>,
|
||||
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
|
||||
}
|
||||
|
||||
impl BarWidget for Komorebi {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
|
||||
|
||||
if self.workspaces.enable {
|
||||
let mut update = None;
|
||||
|
||||
for (i, ws) in komorebi_notification_state.workspaces.iter().enumerate() {
|
||||
if ui
|
||||
.add(SelectableLabel::new(
|
||||
komorebi_notification_state.selected_workspace.eq(ws),
|
||||
ws.to_string(),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
update = Some(ws.to_string());
|
||||
let mut proceed = true;
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: FocusWorkspaceNumber");
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::RetileWithResizeDimensions)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: Retile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(update) = update {
|
||||
komorebi_notification_state.selected_workspace = update;
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if let Some(layout) = self.layout {
|
||||
if layout.enable {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(komorebi_notification_state.layout.to_string())
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
match komorebi_notification_state.layout {
|
||||
KomorebiLayout::Default(_) => {
|
||||
if komorebi_client::send_message(&SocketMessage::CycleLayout(
|
||||
CycleDirection::Next,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: CycleLayout");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Floating => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: ToggleTiling");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Paused => {
|
||||
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
|
||||
tracing::error!("could not send message to komorebi: TogglePause");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Custom => {}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(configuration_switcher) = &self.configuration_switcher {
|
||||
if configuration_switcher.enable {
|
||||
for (name, location) in configuration_switcher.configurations.iter() {
|
||||
let path = PathBuf::from(location);
|
||||
if path.is_file()
|
||||
&& ui
|
||||
.add(Label::new(name).selectable(false).sense(Sense::click()))
|
||||
.clicked()
|
||||
{
|
||||
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
|
||||
let mut proceed = true;
|
||||
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
|
||||
canonicalized,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: ReplaceConfiguration"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if let Some(rect) = komorebi_notification_state.work_area_offset {
|
||||
if proceed {
|
||||
match komorebi_client::send_query(&SocketMessage::Query(
|
||||
komorebi_client::StateQuery::FocusedMonitorIndex,
|
||||
)) {
|
||||
Ok(idx) => {
|
||||
if let Ok(monitor_idx) = idx.parse::<usize>() {
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MonitorWorkAreaOffset"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: Query"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(focused_window) = self.focused_window {
|
||||
if focused_window.enable {
|
||||
let titles = &komorebi_notification_state.focused_container_information.0;
|
||||
let icons = &komorebi_notification_state.focused_container_information.1;
|
||||
let focused_window_idx =
|
||||
komorebi_notification_state.focused_container_information.2;
|
||||
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
|
||||
for (i, (title, icon)) in iter.enumerate() {
|
||||
if focused_window.show_icon {
|
||||
if let Some(img) = icon {
|
||||
ui.add(
|
||||
Image::from(&img_to_texture(ctx, img))
|
||||
.maintain_aspect_ratio(true)
|
||||
.max_height(15.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if i == focused_window_idx {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let layout_job = LayoutJob::simple(
|
||||
title.to_string(),
|
||||
font_id.clone(),
|
||||
komorebi_notification_state
|
||||
.stack_accent
|
||||
.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
if titles.len() > 1 {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job).selectable(false).truncate(),
|
||||
);
|
||||
} else {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title).selectable(false).truncate(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
if custom_ui
|
||||
.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title)
|
||||
.selectable(false)
|
||||
.sense(Sense::click())
|
||||
.truncate(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
false,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(i))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusStackWindow"
|
||||
);
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
|
||||
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||
let pixels = rgba_image.as_flat_samples();
|
||||
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
|
||||
ctx.load_texture("icon", color_image, TextureOptions::default())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationState {
|
||||
pub workspaces: Vec<String>,
|
||||
pub selected_workspace: String,
|
||||
pub focused_container_information: (Vec<String>, Vec<Option<RgbaImage>>, usize),
|
||||
pub layout: KomorebiLayout,
|
||||
pub hide_empty_workspaces: bool,
|
||||
pub mouse_follows_focus: bool,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub stack_accent: Option<Color32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum KomorebiLayout {
|
||||
Default(komorebi_client::DefaultLayout),
|
||||
Floating,
|
||||
Paused,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl Display for KomorebiLayout {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
|
||||
KomorebiLayout::Floating => write!(f, "Floating"),
|
||||
KomorebiLayout::Paused => write!(f, "Paused"),
|
||||
KomorebiLayout::Custom => write!(f, "Custom"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KomorebiNotificationState {
|
||||
pub fn update_from_config(&mut self, config: &Self) {
|
||||
self.hide_empty_workspaces = config.hide_empty_workspaces;
|
||||
}
|
||||
|
||||
pub fn handle_notification(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
monitor_index: usize,
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
) {
|
||||
match rx_gui.try_recv() {
|
||||
Err(error) => match error {
|
||||
TryRecvError::Empty => {}
|
||||
TryRecvError::Disconnected => {
|
||||
tracing::error!(
|
||||
"failed to receive komorebi notification on gui thread: {error}"
|
||||
);
|
||||
}
|
||||
},
|
||||
Ok(notification) => {
|
||||
if let NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(path)) =
|
||||
notification.event
|
||||
{
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.mouse_follows_focus = notification.state.mouse_follows_focus;
|
||||
|
||||
let monitor = ¬ification.state.monitors.elements()[monitor_index];
|
||||
self.work_area_offset =
|
||||
notification.state.monitors.elements()[monitor_index].work_area_offset();
|
||||
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
|
||||
let mut workspaces = vec![];
|
||||
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
|
||||
|
||||
for (i, ws) in monitor.workspaces().iter().enumerate() {
|
||||
let should_add = if self.hide_empty_workspaces {
|
||||
focused_workspace_idx == i || !ws.containers().is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_add {
|
||||
workspaces
|
||||
.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
self.workspaces = workspaces;
|
||||
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
|
||||
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
|
||||
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
|
||||
};
|
||||
|
||||
if !*monitor.workspaces()[focused_workspace_idx].tile() {
|
||||
self.layout = KomorebiLayout::Floating;
|
||||
}
|
||||
|
||||
if notification.state.is_paused {
|
||||
self.layout = KomorebiLayout::Paused;
|
||||
}
|
||||
|
||||
if let Some(container) =
|
||||
monitor.workspaces()[focused_workspace_idx].monocle_container()
|
||||
{
|
||||
self.focused_container_information = (
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.title().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
container.focused_window_idx(),
|
||||
);
|
||||
} else if let Some(container) =
|
||||
monitor.workspaces()[focused_workspace_idx].focused_container()
|
||||
{
|
||||
self.focused_container_information = (
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.title().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
container.focused_window_idx(),
|
||||
);
|
||||
} else {
|
||||
self.focused_container_information.0.clear();
|
||||
self.focused_container_information.1.clear();
|
||||
self.focused_container_information.2 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
411
komorebi-bar/src/main.rs
Normal file
411
komorebi-bar/src/main.rs
Normal file
@@ -0,0 +1,411 @@
|
||||
mod bar;
|
||||
mod battery;
|
||||
mod config;
|
||||
mod cpu;
|
||||
mod date;
|
||||
mod komorebi;
|
||||
mod media;
|
||||
mod memory;
|
||||
mod network;
|
||||
mod storage;
|
||||
mod time;
|
||||
mod ui;
|
||||
mod widget;
|
||||
|
||||
use crate::bar::Komobar;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use clap::Parser;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::SubscribeOptions;
|
||||
use schemars::gen::SchemaSettings;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||
use windows::Win32::System::Threading::GetCurrentThreadId;
|
||||
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
|
||||
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
|
||||
pub static WIDGET_SPACING: f32 = 10.0;
|
||||
|
||||
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
|
||||
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
|
||||
pub static BAR_HEIGHT: f32 = 50.0;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
struct Opts {
|
||||
/// Print the JSON schema of the configuration file and exit
|
||||
#[clap(long)]
|
||||
schema: bool,
|
||||
/// Print a list of fonts available on this system and exit
|
||||
#[clap(long)]
|
||||
fonts: bool,
|
||||
/// Path to a JSON or YAML configuration file
|
||||
#[clap(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Write an example komorebi.bar.json to disk
|
||||
#[clap(long)]
|
||||
quickstart: bool,
|
||||
/// Print a list of aliases that can be renamed to canonical variants
|
||||
#[clap(long)]
|
||||
#[clap(hide = true)]
|
||||
aliases: bool,
|
||||
}
|
||||
|
||||
extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
unsafe {
|
||||
let mut process_id = 0;
|
||||
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
|
||||
|
||||
if process_id == GetCurrentProcessId() {
|
||||
*(lparam.0 as *mut HWND) = hwnd;
|
||||
BOOL::from(false) // Stop enumeration
|
||||
} else {
|
||||
BOOL::from(true) // Continue enumeration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_hwnd() -> Option<isize> {
|
||||
unsafe {
|
||||
let mut hwnd = HWND::default();
|
||||
let _ = EnumThreadWindows(
|
||||
GetCurrentThreadId(),
|
||||
Some(enum_window),
|
||||
LPARAM(&mut hwnd as *mut HWND as isize),
|
||||
);
|
||||
|
||||
if hwnd.0 as isize == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(hwnd.0 as isize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }?;
|
||||
|
||||
let opts: Opts = Opts::parse();
|
||||
|
||||
if opts.schema {
|
||||
let settings = SchemaSettings::default().with(|s| {
|
||||
s.option_nullable = false;
|
||||
s.option_add_null_type = false;
|
||||
s.inline_subschemas = true;
|
||||
});
|
||||
|
||||
let gen = settings.into_generator();
|
||||
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
|
||||
println!("{schema}");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
if opts.fonts {
|
||||
for font in system_fonts::query_all() {
|
||||
println!("{font}");
|
||||
}
|
||||
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::fmt::Subscriber::builder()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.finish(),
|
||||
)?;
|
||||
|
||||
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|
||||
|_| dirs::home_dir().expect("there is no home directory"),
|
||||
|home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if opts.quickstart {
|
||||
let komorebi_bar_json = include_str!("../../docs/komorebi.bar.example.json").to_string();
|
||||
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
|
||||
println!(
|
||||
"Example komorebi.bar.json file written to {}",
|
||||
home_dir.as_path().display()
|
||||
);
|
||||
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let default_config_path = home_dir.join("komorebi.bar.json");
|
||||
|
||||
let config_path = opts.config.map_or_else(
|
||||
|| {
|
||||
if !default_config_path.is_file() {
|
||||
None
|
||||
} else {
|
||||
Some(default_config_path.clone())
|
||||
}
|
||||
},
|
||||
Option::from,
|
||||
);
|
||||
|
||||
let mut config = match config_path {
|
||||
None => {
|
||||
let komorebi_bar_json =
|
||||
include_str!("../../docs/komorebi.bar.example.json").to_string();
|
||||
|
||||
std::fs::write(&default_config_path, komorebi_bar_json)?;
|
||||
tracing::info!(
|
||||
"created example configuration file: {}",
|
||||
default_config_path.as_path().display()
|
||||
);
|
||||
|
||||
KomobarConfig::read(&default_config_path)?
|
||||
}
|
||||
Some(ref config) => {
|
||||
if !opts.aliases {
|
||||
tracing::info!(
|
||||
"found configuration file: {}",
|
||||
config.as_path().to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
KomobarConfig::read(config)?
|
||||
}
|
||||
};
|
||||
|
||||
let config_path = config_path.unwrap_or(default_config_path);
|
||||
|
||||
if opts.aliases {
|
||||
KomobarConfig::aliases(&std::fs::read_to_string(&config_path)?);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let state = serde_json::from_str::<komorebi_client::State>(&komorebi_client::send_query(
|
||||
&SocketMessage::State,
|
||||
)?)?;
|
||||
|
||||
MONITOR_RIGHT.store(
|
||||
state.monitors.elements()[config.monitor.index].size().right,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[config.monitor.index].size().top,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[config.monitor.index].size().left,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
match config.position {
|
||||
None => {
|
||||
config.position = Some(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().left as f32,
|
||||
y: state.monitors.elements()[config.monitor.index].size().top as f32,
|
||||
}),
|
||||
end: Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32,
|
||||
y: 50.0,
|
||||
}),
|
||||
})
|
||||
}
|
||||
Some(ref mut position) => {
|
||||
if position.start.is_none() {
|
||||
position.start = Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().left as f32,
|
||||
y: state.monitors.elements()[config.monitor.index].size().top as f32,
|
||||
});
|
||||
}
|
||||
|
||||
if position.end.is_none() {
|
||||
position.end = Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32,
|
||||
y: 50.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let viewport_builder = ViewportBuilder::default()
|
||||
.with_decorations(false)
|
||||
// .with_transparent(config.transparent)
|
||||
.with_taskbar(false);
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: viewport_builder,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(rect) = &config.monitor.work_area_offset {
|
||||
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
|
||||
config.monitor.index,
|
||||
*rect,
|
||||
))?;
|
||||
tracing::info!(
|
||||
"work area offset applied to monitor: {}",
|
||||
config.monitor.index
|
||||
);
|
||||
}
|
||||
|
||||
let (tx_gui, rx_gui) = crossbeam_channel::unbounded();
|
||||
let (tx_config, rx_config) = crossbeam_channel::unbounded();
|
||||
|
||||
let mut hotwatch = Hotwatch::new()?;
|
||||
let config_path_cl = config_path.clone();
|
||||
|
||||
hotwatch.watch(config_path, move |event| match event.kind {
|
||||
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
|
||||
Ok(updated) => {
|
||||
tracing::info!(
|
||||
"configuration file updated: {}",
|
||||
config_path_cl.as_path().to_string_lossy()
|
||||
);
|
||||
|
||||
if let Err(error) = tx_config.send(updated) {
|
||||
tracing::error!("could not send configuration update to gui: {error}")
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
})?;
|
||||
|
||||
tracing::info!("watching configuration file for changes");
|
||||
|
||||
let config_arc = Arc::new(config);
|
||||
eframe::run_native(
|
||||
"komorebi-bar",
|
||||
native_options,
|
||||
Box::new(|cc| {
|
||||
let config_cl = config_arc.clone();
|
||||
|
||||
let ctx_repainter = cc.egui_ctx.clone();
|
||||
std::thread::spawn(move || loop {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
ctx_repainter.request_repaint();
|
||||
});
|
||||
|
||||
let ctx_komorebi = cc.egui_ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
|
||||
|
||||
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
|
||||
filter_state_changes: true,
|
||||
})
|
||||
.expect("could not subscribe to komorebi notifications");
|
||||
|
||||
tracing::info!("subscribed to komorebi notifications: \"{}\"", subscriber_name);
|
||||
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(subscription) => {
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(subscription);
|
||||
|
||||
// this is when we know a shutdown has been sent
|
||||
if matches!(reader.read_to_end(&mut buffer), Ok(0)) {
|
||||
tracing::info!("disconnected from komorebi");
|
||||
|
||||
// keep trying to reconnect to komorebi
|
||||
while komorebi_client::send_message(
|
||||
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
|
||||
tracing::info!("reconnected to komorebi");
|
||||
|
||||
if let Some(rect) = &config_cl.monitor.work_area_offset {
|
||||
while komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
config_cl.monitor.index,
|
||||
*rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match String::from_utf8(buffer) {
|
||||
Ok(notification_string) => {
|
||||
match serde_json::from_str::<komorebi_client::Notification>(
|
||||
¬ification_string,
|
||||
) {
|
||||
Ok(notification) => {
|
||||
tracing::debug!("received notification from komorebi");
|
||||
|
||||
if let Err(error) = tx_gui.send(notification) {
|
||||
tracing::error!("could not send komorebi notification update to gui thread: {error}")
|
||||
}
|
||||
|
||||
ctx_komorebi.request_repaint();
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("could not deserialize komorebi notification: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(
|
||||
"komorebi notification string was invalid utf8: {error}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config_arc)))
|
||||
}),
|
||||
)
|
||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||
}
|
||||
128
komorebi-bar/src/media.rs
Normal file
128
komorebi-bar/src/media.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MediaConfig {
|
||||
/// Enable the Media widget
|
||||
pub enable: bool,
|
||||
}
|
||||
|
||||
impl From<MediaConfig> for Media {
|
||||
fn from(value: MediaConfig) -> Self {
|
||||
Self::new(value.enable)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Media {
|
||||
pub enable: bool,
|
||||
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
|
||||
}
|
||||
|
||||
impl Media {
|
||||
pub fn new(enable: bool) -> Self {
|
||||
Self {
|
||||
enable,
|
||||
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
|
||||
.unwrap()
|
||||
.get()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
||||
if let Ok(op) = session.TryTogglePlayPauseAsync() {
|
||||
op.get().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn output(&mut self) -> String {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
||||
if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
|
||||
if let Ok(properties) = operation.get() {
|
||||
if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
|
||||
if artist.is_empty() {
|
||||
return format!("{title}");
|
||||
}
|
||||
|
||||
if title.is_empty() {
|
||||
return format!("{artist}");
|
||||
}
|
||||
|
||||
return format!("{artist} - {title}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Media {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
egui_phosphor::regular::HEADPHONES.to_string(),
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
if custom_ui
|
||||
.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click())
|
||||
.truncate(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.toggle();
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
komorebi-bar/src/memory.rs
Normal file
123
komorebi-bar/src/memory.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::RefreshKind;
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MemoryConfig {
|
||||
/// Enable the Memory widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<MemoryConfig> for Memory {
|
||||
fn from(value: MemoryConfig) -> Self {
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
|
||||
|
||||
system.refresh_memory();
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Memory {
|
||||
pub enable: bool,
|
||||
system: System,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
fn output(&mut self) -> String {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.system.refresh_memory();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let used = self.system.used_memory();
|
||||
let total = self.system.total_memory();
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Memory {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::MEMORY.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
425
komorebi-bar/src/network.rs
Normal file
425
komorebi-bar/src/network.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use num_derive::FromPrimitive;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Networks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NetworkConfig {
|
||||
/// Enable the Network widget
|
||||
pub enable: bool,
|
||||
/// Show total data transmitted
|
||||
pub show_total_data_transmitted: bool,
|
||||
/// Show network activity
|
||||
pub show_network_activity: bool,
|
||||
/// Characters to reserve for network activity data
|
||||
pub network_activity_fill_characters: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<NetworkConfig> for Network {
|
||||
fn from(value: NetworkConfig) -> Self {
|
||||
let mut last_state_data = vec![];
|
||||
let mut last_state_transmitted = vec![];
|
||||
|
||||
let mut networks_total_data_transmitted = Networks::new_with_refreshed_list();
|
||||
let mut networks_network_activity = Networks::new_with_refreshed_list();
|
||||
|
||||
let mut default_interface = String::new();
|
||||
|
||||
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = interface.friendly_name {
|
||||
default_interface.clone_from(&friendly_name);
|
||||
|
||||
if value.show_total_data_transmitted {
|
||||
networks_total_data_transmitted.refresh();
|
||||
for (interface_name, data) in &networks_total_data_transmitted {
|
||||
if friendly_name.eq(interface_name) {
|
||||
last_state_data.push(match prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{} | {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {} | {} {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"\u{2211}DOWN: {} | \u{2211}UP: {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::IconAndText => format!(
|
||||
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if value.show_network_activity {
|
||||
networks_network_activity.refresh();
|
||||
for (interface_name, data) in &networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
last_state_transmitted.push(match prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{: >width$}/s | {: >width$}/s",
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {: >width$}/s | {} {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"DOWN: {: >width$}/s | UP: {: >width$}/s",
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
LabelPrefix::IconAndText => format!(
|
||||
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
networks_total_data_transmitted,
|
||||
networks_network_activity,
|
||||
default_interface,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: prefix,
|
||||
show_total_data_transmitted: value.show_total_data_transmitted,
|
||||
show_network_activity: value.show_network_activity,
|
||||
network_activity_fill_characters: value
|
||||
.network_activity_fill_characters
|
||||
.unwrap_or_default(),
|
||||
last_state_total_data_transmitted: last_state_data,
|
||||
last_state_network_activity: last_state_transmitted,
|
||||
last_updated_total_data_transmitted: Instant::now(),
|
||||
last_updated_network_activity: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Network {
|
||||
pub enable: bool,
|
||||
pub show_total_data_transmitted: bool,
|
||||
pub show_network_activity: bool,
|
||||
networks_total_data_transmitted: Networks,
|
||||
networks_network_activity: Networks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
default_interface: String,
|
||||
last_state_total_data_transmitted: Vec<String>,
|
||||
last_state_network_activity: Vec<String>,
|
||||
last_updated_total_data_transmitted: Instant,
|
||||
last_updated_network_activity: Instant,
|
||||
network_activity_fill_characters: usize,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
fn default_interface(&mut self) {
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> Vec<String> {
|
||||
let mut outputs = self.last_state_network_activity.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if self.show_network_activity
|
||||
&& now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
outputs.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
if self.show_network_activity {
|
||||
self.networks_network_activity.refresh();
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
outputs.push(match self.label_prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{: >width$}/s | {: >width$}/s",
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {: >width$}/s | {} {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"DOWN: {: >width$}/s | UP: {: >width$}/s",
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
LabelPrefix::IconAndText => {
|
||||
format!(
|
||||
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_network_activity.clone_from(&outputs);
|
||||
self.last_updated_network_activity = now;
|
||||
}
|
||||
|
||||
outputs
|
||||
}
|
||||
|
||||
fn total_data_transmitted(&mut self) -> Vec<String> {
|
||||
let mut outputs = self.last_state_total_data_transmitted.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if self.show_total_data_transmitted
|
||||
&& now.duration_since(self.last_updated_total_data_transmitted)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
outputs.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
if self.show_total_data_transmitted {
|
||||
self.networks_total_data_transmitted.refresh();
|
||||
|
||||
for (interface_name, data) in &self.networks_total_data_transmitted {
|
||||
if friendly_name.eq(interface_name) {
|
||||
outputs.push(match self.label_prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{} | {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {} | {} {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"\u{2211}DOWN: {} | \u{2211}UP: {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::IconAndText => format!(
|
||||
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_total_data_transmitted.clone_from(&outputs);
|
||||
self.last_updated_total_data_transmitted = now;
|
||||
}
|
||||
|
||||
outputs
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Network {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.show_total_data_transmitted {
|
||||
for output in self.total_data_transmitted() {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if self.show_network_activity {
|
||||
for output in self.network_activity() {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if self.enable {
|
||||
self.default_interface();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::WIFI_HIGH.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
self.default_interface.insert_str(0, "NET: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&self.default_interface,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPrimitive)]
|
||||
enum DataUnit {
|
||||
B = 0,
|
||||
K = 1,
|
||||
M = 2,
|
||||
G = 3,
|
||||
T = 4,
|
||||
P = 5,
|
||||
E = 6,
|
||||
Z = 7,
|
||||
Y = 8,
|
||||
}
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
|
||||
let input = input_in_bytes as f32 / timespan_in_s as f32;
|
||||
let mut magnitude = input.log(1024f32) as u32;
|
||||
|
||||
// let the base unit be KiB
|
||||
if magnitude < 1 {
|
||||
magnitude = 1;
|
||||
}
|
||||
|
||||
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
|
||||
let result = input / ((1u64) << (magnitude * 10)) as f32;
|
||||
|
||||
match base {
|
||||
Some(DataUnit::B) => format!("{result:.1} B"),
|
||||
Some(unit) => format!("{result:.1} {unit}iB"),
|
||||
None => String::from("Unknown data unit"),
|
||||
}
|
||||
}
|
||||
134
komorebi-bar/src/storage.rs
Normal file
134
komorebi-bar/src/storage.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Disks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StorageConfig {
|
||||
/// Enable the Storage widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<StorageConfig> for Storage {
|
||||
fn from(value: StorageConfig) -> Self {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
disks: Disks::new_with_refreshed_list(),
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Storage {
|
||||
pub enable: bool,
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
fn output(&mut self) -> Vec<String> {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.disks.refresh();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let mut disks = vec![];
|
||||
|
||||
for disk in &self.disks {
|
||||
let mount = disk.mount_point();
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
let used = total - available;
|
||||
|
||||
disks.push(match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
})
|
||||
}
|
||||
|
||||
disks.sort();
|
||||
disks.reverse();
|
||||
|
||||
disks
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Storage {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
for output in self.output() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::HARD_DRIVES.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
komorebi-bar/src/time.rs
Normal file
128
komorebi-bar/src/time.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TimeConfig {
|
||||
/// Enable the Time widget
|
||||
pub enable: bool,
|
||||
/// Set the Time format
|
||||
pub format: TimeFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<TimeConfig> for Time {
|
||||
fn from(value: TimeConfig) -> Self {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum TimeFormat {
|
||||
/// Twelve-hour format (with seconds)
|
||||
TwelveHour,
|
||||
/// Twenty-four-hour format (with seconds)
|
||||
TwentyFourHour,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl TimeFormat {
|
||||
pub fn toggle(&mut self) {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
|
||||
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn fmt_string(&self) -> String {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
|
||||
TimeFormat::TwentyFourHour => String::from("%T"),
|
||||
TimeFormat::Custom(format) => format.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Time {
|
||||
pub enable: bool,
|
||||
pub format: TimeFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
}
|
||||
|
||||
impl Time {
|
||||
fn output(&mut self) -> String {
|
||||
chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Time {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CLOCK.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.insert_str(0, "TIME: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
komorebi-bar/src/ui.rs
Normal file
22
komorebi-bar/src/ui.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Layout;
|
||||
use eframe::egui::Response;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::Widget;
|
||||
|
||||
pub struct CustomUi<'ui>(pub &'ui mut Ui);
|
||||
|
||||
impl CustomUi<'_> {
|
||||
pub fn add_sized_left_to_right(
|
||||
&mut self,
|
||||
max_size: impl Into<Vec2>,
|
||||
widget: impl Widget,
|
||||
) -> Response {
|
||||
let layout =
|
||||
Layout::from_main_dir_and_cross_align(self.0.layout().main_dir(), Align::Center);
|
||||
self.0
|
||||
.allocate_ui_with_layout(max_size.into(), layout, |ui| ui.add(widget))
|
||||
.inner
|
||||
}
|
||||
}
|
||||
56
komorebi-bar/src/widget.rs
Normal file
56
komorebi-bar/src/widget.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::battery::Battery;
|
||||
use crate::battery::BatteryConfig;
|
||||
use crate::cpu::Cpu;
|
||||
use crate::cpu::CpuConfig;
|
||||
use crate::date::Date;
|
||||
use crate::date::DateConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiConfig;
|
||||
use crate::media::Media;
|
||||
use crate::media::MediaConfig;
|
||||
use crate::memory::Memory;
|
||||
use crate::memory::MemoryConfig;
|
||||
use crate::network::Network;
|
||||
use crate::network::NetworkConfig;
|
||||
use crate::storage::Storage;
|
||||
use crate::storage::StorageConfig;
|
||||
use crate::time::Time;
|
||||
use crate::time::TimeConfig;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub trait BarWidget {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum WidgetConfig {
|
||||
Battery(BatteryConfig),
|
||||
Cpu(CpuConfig),
|
||||
Date(DateConfig),
|
||||
Komorebi(KomorebiConfig),
|
||||
Media(MediaConfig),
|
||||
Memory(MemoryConfig),
|
||||
Network(NetworkConfig),
|
||||
Storage(StorageConfig),
|
||||
Time(TimeConfig),
|
||||
}
|
||||
|
||||
impl WidgetConfig {
|
||||
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
|
||||
match self {
|
||||
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
|
||||
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
|
||||
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
|
||||
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
|
||||
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
|
||||
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
|
||||
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
|
||||
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
|
||||
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.26"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi = { path = "../komorebi" }
|
||||
komorebi-core = { path = "../komorebi-core" }
|
||||
uds_windows = "1"
|
||||
|
||||
uds_windows = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||
pub use komorebi::colour::Colour;
|
||||
pub use komorebi::colour::Rgb;
|
||||
pub use komorebi::config_generation::ApplicationConfiguration;
|
||||
pub use komorebi::container::Container;
|
||||
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
||||
pub use komorebi::core::resolve_home_path;
|
||||
pub use komorebi::core::AnimationStyle;
|
||||
pub use komorebi::core::ApplicationIdentifier;
|
||||
pub use komorebi::core::Arrangement;
|
||||
pub use komorebi::core::Axis;
|
||||
pub use komorebi::core::BorderImplementation;
|
||||
pub use komorebi::core::BorderStyle;
|
||||
pub use komorebi::core::CustomLayout;
|
||||
pub use komorebi::core::CycleDirection;
|
||||
pub use komorebi::core::DefaultLayout;
|
||||
pub use komorebi::core::Direction;
|
||||
pub use komorebi::core::FocusFollowsMouseImplementation;
|
||||
pub use komorebi::core::HidingBehaviour;
|
||||
pub use komorebi::core::Layout;
|
||||
pub use komorebi::core::MoveBehaviour;
|
||||
pub use komorebi::core::OperationBehaviour;
|
||||
pub use komorebi::core::OperationDirection;
|
||||
pub use komorebi::core::Rect;
|
||||
pub use komorebi::core::Sizing;
|
||||
pub use komorebi::core::SocketMessage;
|
||||
pub use komorebi::core::StackbarLabel;
|
||||
pub use komorebi::core::StackbarMode;
|
||||
pub use komorebi::core::StateQuery;
|
||||
pub use komorebi::core::WindowKind;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
pub use komorebi::ring::Ring;
|
||||
pub use komorebi::window::Window;
|
||||
@@ -11,27 +38,15 @@ pub use komorebi::window_manager_event::WindowManagerEvent;
|
||||
pub use komorebi::workspace::Workspace;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::GlobalState;
|
||||
pub use komorebi::KomorebiTheme;
|
||||
pub use komorebi::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::RuleDebug;
|
||||
pub use komorebi::StackbarConfig;
|
||||
pub use komorebi::State;
|
||||
pub use komorebi::StaticConfig;
|
||||
pub use komorebi::SubscribeOptions;
|
||||
pub use komorebi::TabsConfig;
|
||||
pub use komorebi_core::Arrangement;
|
||||
pub use komorebi_core::Axis;
|
||||
pub use komorebi_core::BorderStyle;
|
||||
pub use komorebi_core::CustomLayout;
|
||||
pub use komorebi_core::CycleDirection;
|
||||
pub use komorebi_core::DefaultLayout;
|
||||
pub use komorebi_core::Direction;
|
||||
pub use komorebi_core::Layout;
|
||||
pub use komorebi_core::OperationDirection;
|
||||
pub use komorebi_core::Rect;
|
||||
pub use komorebi_core::SocketMessage;
|
||||
pub use komorebi_core::StackbarLabel;
|
||||
pub use komorebi_core::StackbarMode;
|
||||
pub use komorebi_core::WindowKind;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
@@ -46,16 +61,10 @@ const KOMOREBI: &str = "komorebi.sock";
|
||||
|
||||
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut connected = false;
|
||||
while !connected {
|
||||
if let Ok(mut stream) = UnixStream::connect(&socket) {
|
||||
connected = true;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
|
||||
@@ -89,3 +98,29 @@ pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
pub fn subscribe_with_options(
|
||||
name: &str,
|
||||
options: SubscribeOptions,
|
||||
) -> std::io::Result<UnixListener> {
|
||||
let socket = DATA_DIR.join(name);
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(()) => {}
|
||||
Err(error) => match error.kind() {
|
||||
std::io::ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
|
||||
send_message(&SocketMessage::AddSubscriberSocketWithOptions(
|
||||
name.to_string(),
|
||||
options,
|
||||
))?;
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "komorebi-core"
|
||||
version = "0.1.26"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
color-eyre = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
@@ -1,14 +1,15 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.26"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
egui_extras = { version = "0.27" }
|
||||
eframe = "0.27"
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
serde_json = "1"
|
||||
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
random_word = { version = "0.4.3", features = ["en"] }
|
||||
serde_json = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
@@ -1,3 +1,5 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
use eframe::egui;
|
||||
use eframe::egui::color_picker::Alpha;
|
||||
use eframe::egui::Color32;
|
||||
@@ -25,14 +27,13 @@ fn main() {
|
||||
viewport: ViewportBuilder::default()
|
||||
.with_always_on_top()
|
||||
.with_inner_size([320.0, 500.0]),
|
||||
follow_system_theme: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _ = eframe::run_native(
|
||||
"komorebi-gui",
|
||||
native_options,
|
||||
Box::new(|cc| Box::new(KomorebiGui::new(cc))),
|
||||
Box::new(|cc| Ok(Box::new(KomorebiGui::new(cc)))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,7 +217,7 @@ extern "system" fn enum_window(
|
||||
lparam: windows::Win32::Foundation::LPARAM,
|
||||
) -> windows::Win32::Foundation::BOOL {
|
||||
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
|
||||
let window = Window { hwnd: hwnd.0 };
|
||||
let window = Window::from(hwnd.0 as isize);
|
||||
|
||||
if window.is_window()
|
||||
&& !window.is_miminized()
|
||||
@@ -232,7 +233,8 @@ extern "system" fn enum_window(
|
||||
|
||||
fn json_view_ui(ui: &mut egui::Ui, code: &str) {
|
||||
let language = "json";
|
||||
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
let theme =
|
||||
egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), &ui.ctx().style());
|
||||
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
|
||||
}
|
||||
|
||||
@@ -244,9 +246,7 @@ impl eframe::App for KomorebiGui {
|
||||
ui.set_width(ctx.screen_rect().width());
|
||||
ui.collapsing("Debugging", |ui| {
|
||||
ui.collapsing("Window Rules", |ui| {
|
||||
let window = Window {
|
||||
hwnd: self.debug_hwnd,
|
||||
};
|
||||
let window = Window::from(self.debug_hwnd);
|
||||
|
||||
let label = if let (Ok(title), Ok(exe)) = (window.title(), window.exe()) {
|
||||
format!("{title} ({exe})")
|
||||
@@ -713,7 +713,7 @@ impl eframe::App for KomorebiGui {
|
||||
.text_edit_singleline(workspace_name)
|
||||
.lost_focus()
|
||||
{
|
||||
workspace.name = workspace_name.clone();
|
||||
workspace.name.clone_from(workspace_name);
|
||||
komorebi_client::send_message(
|
||||
&SocketMessage::WorkspaceName(
|
||||
monitor_idx,
|
||||
|
||||
12
komorebi-themes/Cargo.toml
Normal file
12
komorebi-themes/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "57c38257cb0c6434321320d3746049bd58c34674" }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f579847bf2f552b144361d5a78ed8cf360b55cbb" }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui28"] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
162
komorebi-themes/src/lib.rs
Normal file
162
komorebi-themes/src/lib.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub use base16_egui_themes::Base16;
|
||||
pub use catppuccin_egui;
|
||||
pub use eframe::egui::Color32;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Theme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
name: Catppuccin,
|
||||
accent: Option<CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
name: Base16,
|
||||
accent: Option<Base16Value>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Base16Value {
|
||||
Base00,
|
||||
Base01,
|
||||
Base02,
|
||||
Base03,
|
||||
Base04,
|
||||
Base05,
|
||||
#[default]
|
||||
Base06,
|
||||
Base07,
|
||||
Base08,
|
||||
Base09,
|
||||
Base0A,
|
||||
Base0B,
|
||||
Base0C,
|
||||
Base0D,
|
||||
Base0E,
|
||||
Base0F,
|
||||
}
|
||||
|
||||
impl Base16Value {
|
||||
pub fn color32(&self, theme: Base16) -> Color32 {
|
||||
match self {
|
||||
Base16Value::Base00 => theme.base00(),
|
||||
Base16Value::Base01 => theme.base01(),
|
||||
Base16Value::Base02 => theme.base02(),
|
||||
Base16Value::Base03 => theme.base03(),
|
||||
Base16Value::Base04 => theme.base04(),
|
||||
Base16Value::Base05 => theme.base05(),
|
||||
Base16Value::Base06 => theme.base06(),
|
||||
Base16Value::Base07 => theme.base07(),
|
||||
Base16Value::Base08 => theme.base08(),
|
||||
Base16Value::Base09 => theme.base09(),
|
||||
Base16Value::Base0A => theme.base0a(),
|
||||
Base16Value::Base0B => theme.base0b(),
|
||||
Base16Value::Base0C => theme.base0c(),
|
||||
Base16Value::Base0D => theme.base0d(),
|
||||
Base16Value::Base0E => theme.base0e(),
|
||||
Base16Value::Base0F => theme.base0f(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Catppuccin {
|
||||
Frappe,
|
||||
Latte,
|
||||
Macchiato,
|
||||
Mocha,
|
||||
}
|
||||
|
||||
impl Catppuccin {
|
||||
pub fn as_theme(self) -> catppuccin_egui::Theme {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Catppuccin> for catppuccin_egui::Theme {
|
||||
fn from(val: Catppuccin) -> Self {
|
||||
match val {
|
||||
Catppuccin::Frappe => catppuccin_egui::FRAPPE,
|
||||
Catppuccin::Latte => catppuccin_egui::LATTE,
|
||||
Catppuccin::Macchiato => catppuccin_egui::MACCHIATO,
|
||||
Catppuccin::Mocha => catppuccin_egui::MOCHA,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum CatppuccinValue {
|
||||
Rosewater,
|
||||
Flamingo,
|
||||
Pink,
|
||||
Mauve,
|
||||
Red,
|
||||
Maroon,
|
||||
Peach,
|
||||
Yellow,
|
||||
Green,
|
||||
Teal,
|
||||
Sky,
|
||||
Sapphire,
|
||||
Blue,
|
||||
Lavender,
|
||||
#[default]
|
||||
Text,
|
||||
Subtext1,
|
||||
Subtext0,
|
||||
Overlay2,
|
||||
Overlay1,
|
||||
Overlay0,
|
||||
Surface2,
|
||||
Surface1,
|
||||
Surface0,
|
||||
Base,
|
||||
Mantle,
|
||||
Crust,
|
||||
}
|
||||
|
||||
pub fn color32_compat(rgba: [u8; 4]) -> Color32 {
|
||||
Color32::from_rgba_unmultiplied(rgba[0], rgba[1], rgba[2], rgba[3])
|
||||
}
|
||||
|
||||
impl CatppuccinValue {
|
||||
pub fn color32(&self, theme: catppuccin_egui::Theme) -> Color32 {
|
||||
match self {
|
||||
CatppuccinValue::Rosewater => color32_compat(theme.rosewater.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Flamingo => color32_compat(theme.flamingo.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Pink => color32_compat(theme.pink.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Mauve => color32_compat(theme.mauve.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Red => color32_compat(theme.red.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Maroon => color32_compat(theme.maroon.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Peach => color32_compat(theme.peach.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Yellow => color32_compat(theme.yellow.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Green => color32_compat(theme.green.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Teal => color32_compat(theme.teal.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Sky => color32_compat(theme.sky.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Sapphire => color32_compat(theme.sapphire.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Blue => color32_compat(theme.blue.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Lavender => color32_compat(theme.lavender.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Text => color32_compat(theme.text.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Subtext1 => color32_compat(theme.subtext1.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Subtext0 => color32_compat(theme.subtext0.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Overlay2 => color32_compat(theme.overlay2.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Overlay1 => color32_compat(theme.overlay1.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Overlay0 => color32_compat(theme.overlay0.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Surface2 => color32_compat(theme.surface2.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Surface1 => color32_compat(theme.surface1.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Surface0 => color32_compat(theme.surface0.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Base => color32_compat(theme.base.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Mantle => color32_compat(theme.mantle.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Crust => color32_compat(theme.crust.to_srgba_unmultiplied()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
#SingleInstance Force
|
||||
|
||||
; Load library
|
||||
#Include komorebic.lib.ahk
|
||||
|
||||
; Focus windows
|
||||
!h::Focus("left")
|
||||
!j::Focus("down")
|
||||
!k::Focus("up")
|
||||
!l::Focus("right")
|
||||
!+[::CycleFocus("previous")
|
||||
!+]::CycleFocus("next")
|
||||
|
||||
; Move windows
|
||||
!+h::Move("left")
|
||||
!+j::Move("down")
|
||||
!+k::Move("up")
|
||||
!+l::Move("right")
|
||||
!+Enter::Promote()
|
||||
|
||||
; Stack windows
|
||||
!Left::Stack("left")
|
||||
!Right::Stack("right")
|
||||
!Up::Stack("up")
|
||||
!Down::Stack("down")
|
||||
!;::Unstack()
|
||||
![::CycleStack("previous")
|
||||
!]::CycleStack("next")
|
||||
|
||||
; Resize
|
||||
!=::ResizeAxis("horizontal", "increase")
|
||||
!-::ResizeAxis("horizontal", "decrease")
|
||||
!+=::ResizeAxis("vertical", "increase")
|
||||
!+-::ResizeAxis("vertical", "decrease")
|
||||
|
||||
; Manipulate windows
|
||||
!t::ToggleFloat()
|
||||
!+f::ToggleMonocle()
|
||||
|
||||
; Window manager options
|
||||
!+r::Retile()
|
||||
!p::TogglePause()
|
||||
|
||||
; Layouts
|
||||
!x::FlipLayout("horizontal")
|
||||
!y::FlipLayout("vertical")
|
||||
|
||||
; Workspaces
|
||||
!1::FocusWorkspace(0)
|
||||
!2::FocusWorkspace(1)
|
||||
!3::FocusWorkspace(2)
|
||||
|
||||
; Move windows across workspaces
|
||||
!+1::MoveToWorkspace(0)
|
||||
!+2::MoveToWorkspace(1)
|
||||
!+3::MoveToWorkspace(2)
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.26"
|
||||
version = "0.1.30"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
@@ -11,44 +11,50 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-core = { path = "../komorebi-core" }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
|
||||
bitflags = { version = "2", features = ["serde"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
clap = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
ctrlc = "3"
|
||||
crossbeam-channel = { workspace = true }
|
||||
crossbeam-utils = { workspace = true }
|
||||
ctrlc = { version = "3", features = ["termination"] }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
getset = "0.1"
|
||||
hex_color = { version = "3", features = ["serde"] }
|
||||
hotwatch = "0.5"
|
||||
lazy_static = "1"
|
||||
hotwatch = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
miow = "0.6"
|
||||
nanoid = "0.4"
|
||||
net2 = "0.2"
|
||||
os_info = "3.8"
|
||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||
paste = "1"
|
||||
parking_lot = "0.12"
|
||||
paste = { workspace = true }
|
||||
regex = "1"
|
||||
schemars = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
shadow-rs = { workspace = true }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
sysinfo = { workspace = true }
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uds_windows = "1"
|
||||
which = "6"
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
uds_windows = { workspace = true }
|
||||
which = { workspace = true }
|
||||
widestring = "1"
|
||||
win32-display-data = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.52"
|
||||
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data" }
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
[features]
|
||||
deadlock_detection = []
|
||||
deadlock_detection = ["parking_lot/deadlock_detection"]
|
||||
|
||||
3
komorebi/build.rs
Normal file
3
komorebi/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
shadow_rs::new().unwrap();
|
||||
}
|
||||
517
komorebi/src/animation.rs
Normal file
517
komorebi/src/animation.rs
Normal file
@@ -0,0 +1,517 @@
|
||||
use crate::core::AnimationStyle;
|
||||
use crate::core::Rect;
|
||||
use color_eyre::Result;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::f64::consts::PI;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_MANAGER;
|
||||
use crate::ANIMATION_STYLE;
|
||||
|
||||
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(60);
|
||||
|
||||
pub trait Ease {
|
||||
fn evaluate(t: f64) -> f64;
|
||||
}
|
||||
|
||||
pub struct Linear;
|
||||
|
||||
impl Ease for Linear {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInSine;
|
||||
|
||||
impl Ease for EaseInSine {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - f64::cos((t * PI) / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutSine;
|
||||
|
||||
impl Ease for EaseOutSine {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
f64::sin((t * PI) / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutSine;
|
||||
|
||||
impl Ease for EaseInOutSine {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
-(f64::cos(PI * t) - 1.0) / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInQuad;
|
||||
|
||||
impl Ease for EaseInQuad {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutQuad;
|
||||
|
||||
impl Ease for EaseOutQuad {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
(1.0 - t).mul_add(-1.0 - t, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutQuad;
|
||||
|
||||
impl Ease for EaseInOutQuad {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
2.0 * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(2) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInCubic;
|
||||
|
||||
impl Ease for EaseInCubic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutCubic;
|
||||
|
||||
impl Ease for EaseOutCubic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - (1.0 - t).powi(3)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutCubic;
|
||||
|
||||
impl Ease for EaseInOutCubic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
4.0 * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(3) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInQuart;
|
||||
|
||||
impl Ease for EaseInQuart {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t * t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutQuart;
|
||||
|
||||
impl Ease for EaseOutQuart {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - (1.0 - t).powi(4)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutQuart;
|
||||
|
||||
impl Ease for EaseInOutQuart {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
8.0 * t * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(4) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInQuint;
|
||||
|
||||
impl Ease for EaseInQuint {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
t * t * t * t * t
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutQuint;
|
||||
|
||||
impl Ease for EaseOutQuint {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - (1.0 - t).powi(5)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutQuint;
|
||||
|
||||
impl Ease for EaseInOutQuint {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
16.0 * t * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0f64).mul_add(t, 2.0).powi(5) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInExpo;
|
||||
|
||||
impl Ease for EaseInExpo {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
10.0f64.mul_add(t, -10.0).exp2()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutExpo;
|
||||
|
||||
impl Ease for EaseOutExpo {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON {
|
||||
return t;
|
||||
}
|
||||
|
||||
1.0 - (-10.0 * t).exp2()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutExpo;
|
||||
|
||||
impl Ease for EaseInOutExpo {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t == 0.0 || (t - 1.0).abs() < f64::EPSILON {
|
||||
return t;
|
||||
}
|
||||
|
||||
if t < 0.5 {
|
||||
20.0f64.mul_add(t, -10.0).exp2() / 2.0
|
||||
} else {
|
||||
(2.0 - (-20.0f64).mul_add(t, 10.0).exp2()) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInCirc;
|
||||
|
||||
impl Ease for EaseInCirc {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - f64::sqrt(t.mul_add(-t, 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutCirc;
|
||||
|
||||
impl Ease for EaseOutCirc {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
f64::sqrt((t - 1.0).mul_add(-(t - 1.0), 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutCirc;
|
||||
|
||||
impl Ease for EaseInOutCirc {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
(1.0 - f64::sqrt((2.0 * t).mul_add(-(2.0 * t), 1.0))) / 2.0
|
||||
} else {
|
||||
(f64::sqrt(
|
||||
(-2.0f64)
|
||||
.mul_add(t, 2.0)
|
||||
.mul_add(-(-2.0f64).mul_add(t, 2.0), 1.0),
|
||||
) + 1.0)
|
||||
/ 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInBack;
|
||||
|
||||
impl Ease for EaseInBack {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let c1 = 1.70158;
|
||||
let c3 = c1 + 1.0;
|
||||
|
||||
(c3 * t * t).mul_add(t, -c1 * t * t)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutBack;
|
||||
|
||||
impl Ease for EaseOutBack {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let c1: f64 = 1.70158;
|
||||
let c3: f64 = c1 + 1.0;
|
||||
|
||||
c1.mul_add((t - 1.0).powi(2), c3.mul_add((t - 1.0).powi(3), 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutBack;
|
||||
|
||||
impl Ease for EaseInOutBack {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let c1: f64 = 1.70158;
|
||||
let c2: f64 = c1 * 1.525;
|
||||
|
||||
if t < 0.5 {
|
||||
((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0).mul_add(t, -c2)) / 2.0
|
||||
} else {
|
||||
((2.0f64.mul_add(t, -2.0))
|
||||
.powi(2)
|
||||
.mul_add((c2 + 1.0).mul_add(t.mul_add(2.0, -2.0), c2), 2.0))
|
||||
/ 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInElastic;
|
||||
|
||||
impl Ease for EaseInElastic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
let c4 = (2.0 * PI) / 3.0;
|
||||
|
||||
-(10.0f64.mul_add(t, -10.0).exp2()) * f64::sin(t.mul_add(10.0, -10.75) * c4)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutElastic;
|
||||
|
||||
impl Ease for EaseOutElastic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
let c4 = (2.0 * PI) / 3.0;
|
||||
|
||||
(-10.0 * t)
|
||||
.exp2()
|
||||
.mul_add(f64::sin(t.mul_add(10.0, -0.75) * c4), 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutElastic;
|
||||
|
||||
impl Ease for EaseInOutElastic {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if (t - 1.0).abs() < f64::EPSILON || t == 0.0 {
|
||||
return t;
|
||||
}
|
||||
|
||||
let c5 = (2.0 * PI) / 4.5;
|
||||
|
||||
if t < 0.5 {
|
||||
-(20.0f64.mul_add(t, -10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0
|
||||
} else {
|
||||
((-20.0f64).mul_add(t, 10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0
|
||||
+ 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInBounce;
|
||||
|
||||
impl Ease for EaseInBounce {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
1.0 - EaseOutBounce::evaluate(1.0 - t)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutBounce;
|
||||
|
||||
impl Ease for EaseOutBounce {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
let mut time = t;
|
||||
let n1 = 7.5625;
|
||||
let d1 = 2.75;
|
||||
|
||||
if t < 1.0 / d1 {
|
||||
n1 * time * time
|
||||
} else if time < 2.0 / d1 {
|
||||
time -= 1.5 / d1;
|
||||
(n1 * time).mul_add(time, 0.75)
|
||||
} else if time < 2.5 / d1 {
|
||||
time -= 2.25 / d1;
|
||||
(n1 * time).mul_add(time, 0.9375)
|
||||
} else {
|
||||
time -= 2.625 / d1;
|
||||
(n1 * time).mul_add(time, 0.984_375)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutBounce;
|
||||
|
||||
impl Ease for EaseInOutBounce {
|
||||
fn evaluate(t: f64) -> f64 {
|
||||
if t < 0.5 {
|
||||
(1.0 - EaseOutBounce::evaluate(2.0f64.mul_add(-t, 1.0))) / 2.0
|
||||
} else {
|
||||
(1.0 + EaseOutBounce::evaluate(2.0f64.mul_add(t, -1.0))) / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
fn apply_ease_func(t: f64) -> f64 {
|
||||
let style = *ANIMATION_STYLE.lock();
|
||||
|
||||
match style {
|
||||
AnimationStyle::Linear => Linear::evaluate(t),
|
||||
AnimationStyle::EaseInSine => EaseInSine::evaluate(t),
|
||||
AnimationStyle::EaseOutSine => EaseOutSine::evaluate(t),
|
||||
AnimationStyle::EaseInOutSine => EaseInOutSine::evaluate(t),
|
||||
AnimationStyle::EaseInQuad => EaseInQuad::evaluate(t),
|
||||
AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
|
||||
AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t),
|
||||
AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
|
||||
AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t),
|
||||
AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuart => EaseInOutQuart::evaluate(t),
|
||||
AnimationStyle::EaseInQuint => EaseInQuint::evaluate(t),
|
||||
AnimationStyle::EaseOutQuint => EaseOutQuint::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuint => EaseInOutQuint::evaluate(t),
|
||||
AnimationStyle::EaseInExpo => EaseInExpo::evaluate(t),
|
||||
AnimationStyle::EaseOutExpo => EaseOutExpo::evaluate(t),
|
||||
AnimationStyle::EaseInOutExpo => EaseInOutExpo::evaluate(t),
|
||||
AnimationStyle::EaseInCirc => EaseInCirc::evaluate(t),
|
||||
AnimationStyle::EaseOutCirc => EaseOutCirc::evaluate(t),
|
||||
AnimationStyle::EaseInOutCirc => EaseInOutCirc::evaluate(t),
|
||||
AnimationStyle::EaseInBack => EaseInBack::evaluate(t),
|
||||
AnimationStyle::EaseOutBack => EaseOutBack::evaluate(t),
|
||||
AnimationStyle::EaseInOutBack => EaseInOutBack::evaluate(t),
|
||||
AnimationStyle::EaseInElastic => EaseInElastic::evaluate(t),
|
||||
AnimationStyle::EaseOutElastic => EaseOutElastic::evaluate(t),
|
||||
AnimationStyle::EaseInOutElastic => EaseInOutElastic::evaluate(t),
|
||||
AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t),
|
||||
AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
|
||||
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Animation {
|
||||
pub hwnd: isize,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(hwnd: isize) -> Self {
|
||||
Self { hwnd }
|
||||
}
|
||||
|
||||
/// Returns true if the animation needs to continue
|
||||
pub fn cancel(&mut self) -> bool {
|
||||
if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// should be more than 0
|
||||
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(self.hwnd);
|
||||
let max_duration = Duration::from_secs(1);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(
|
||||
ANIMATION_DURATION.load(Ordering::SeqCst) / 2,
|
||||
));
|
||||
}
|
||||
|
||||
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(self.hwnd);
|
||||
|
||||
ANIMATION_MANAGER.lock().end_cancel(self.hwnd);
|
||||
|
||||
latest_cancel_idx == cancel_idx
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn lerp(start: i32, end: i32, t: f64) -> i32 {
|
||||
let time = apply_ease_func(t);
|
||||
f64::from(end - start)
|
||||
.mul_add(time, f64::from(start))
|
||||
.round() as i32
|
||||
}
|
||||
|
||||
pub fn lerp_rect(start_rect: &Rect, end_rect: &Rect, t: f64) -> Rect {
|
||||
Rect {
|
||||
left: Self::lerp(start_rect.left, end_rect.left, t),
|
||||
top: Self::lerp(start_rect.top, end_rect.top, t),
|
||||
right: Self::lerp(start_rect.right, end_rect.right, t),
|
||||
bottom: Self::lerp(start_rect.bottom, end_rect.bottom, t),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
&mut self,
|
||||
duration: Duration,
|
||||
mut render_callback: impl FnMut(f64) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
if ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
let should_animate = self.cancel();
|
||||
|
||||
if !should_animate {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().start(self.hwnd);
|
||||
|
||||
let target_frame_time = Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
|
||||
let mut progress = 0.0;
|
||||
let animation_start = Instant::now();
|
||||
|
||||
// start animation
|
||||
while progress < 1.0 {
|
||||
// check if animation is cancelled
|
||||
if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) {
|
||||
// cancel animation
|
||||
ANIMATION_MANAGER.lock().cancel(self.hwnd);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame_start = Instant::now();
|
||||
// calculate progress
|
||||
progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
|
||||
render_callback(progress).ok();
|
||||
|
||||
// sleep until next frame
|
||||
let frame_time_elapsed = frame_start.elapsed();
|
||||
|
||||
if frame_time_elapsed < target_frame_time {
|
||||
std::thread::sleep(target_frame_time - frame_time_elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
|
||||
// limit progress to 1.0 if animation took longer
|
||||
if progress > 1.0 {
|
||||
progress = 1.0;
|
||||
}
|
||||
|
||||
// process animation for 1.0 to set target position
|
||||
render_callback(progress)
|
||||
}
|
||||
}
|
||||
108
komorebi/src/animation_manager.rs
Normal file
108
komorebi/src/animation_manager.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub static ANIMATIONS_IN_PROGRESS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct AnimationState {
|
||||
pub in_progress: bool,
|
||||
pub cancel_idx_counter: usize,
|
||||
pub pending_cancel_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AnimationManager {
|
||||
animations: HashMap<isize, AnimationState>,
|
||||
}
|
||||
|
||||
impl Default for AnimationManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
animations: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self, hwnd: isize) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(&hwnd) {
|
||||
animation_state.pending_cancel_count > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_progress(&self, hwnd: isize) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(&hwnd) {
|
||||
animation_state.in_progress
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_cancel(&mut self, hwnd: isize) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.pending_cancel_count += 1;
|
||||
animation_state.cancel_idx_counter += 1;
|
||||
|
||||
// return cancel idx
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn latest_cancel_idx(&mut self, hwnd: isize) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_cancel(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.pending_cancel_count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, hwnd: isize) {
|
||||
if let Entry::Vacant(e) = self.animations.entry(hwnd) {
|
||||
e.insert(AnimationState {
|
||||
in_progress: true,
|
||||
cancel_idx_counter: 0,
|
||||
pending_cancel_count: 0,
|
||||
});
|
||||
|
||||
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = false;
|
||||
|
||||
if animation_state.pending_cancel_count == 0 {
|
||||
self.animations.remove(&hwnd);
|
||||
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::FOCUSED;
|
||||
use crate::border_manager::FOCUS_STATE;
|
||||
use crate::border_manager::MONOCLE;
|
||||
use crate::border_manager::RECT_STATE;
|
||||
use crate::border_manager::STACK;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::UNFOCUSED;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
use komorebi_core::BorderStyle;
|
||||
use komorebi_core::Rect;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
@@ -27,12 +24,12 @@ use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Gdi::BeginPaint;
|
||||
use windows::Win32::Graphics::Gdi::CreatePen;
|
||||
use windows::Win32::Graphics::Gdi::DeleteObject;
|
||||
use windows::Win32::Graphics::Gdi::EndPaint;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::RoundRect;
|
||||
use windows::Win32::Graphics::Gdi::SelectObject;
|
||||
use windows::Win32::Graphics::Gdi::ValidateRect;
|
||||
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
|
||||
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
@@ -50,10 +47,11 @@ use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
|
||||
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||
let hwnd = hwnd.0 as isize;
|
||||
|
||||
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
|
||||
if class.starts_with("komoborder") {
|
||||
hwnds.push(hwnd.0);
|
||||
hwnds.push(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +71,7 @@ impl From<isize> for Border {
|
||||
|
||||
impl Border {
|
||||
pub const fn hwnd(&self) -> HWND {
|
||||
HWND(self.hwnd)
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
}
|
||||
|
||||
pub fn create(id: &str) -> color_eyre::Result<Self> {
|
||||
@@ -95,17 +93,25 @@ impl Border {
|
||||
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
|
||||
let instance = h_module.0 as isize;
|
||||
std::thread::spawn(move || -> color_eyre::Result<()> {
|
||||
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), h_module)?;
|
||||
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
|
||||
let mut message = MSG::default();
|
||||
unsafe {
|
||||
while GetMessageW(&mut message, HWND(hwnd), 0, 0).into() {
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
let mut msg: MSG = MSG::default();
|
||||
|
||||
loop {
|
||||
unsafe {
|
||||
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
|
||||
tracing::debug!("border window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
// TODO: error handling
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -117,28 +123,25 @@ impl Border {
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
WindowsApi::close_window(self.hwnd())
|
||||
WindowsApi::close_window(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn update(&self, rect: &Rect) -> color_eyre::Result<()> {
|
||||
pub fn update(&self, rect: &Rect, mut should_invalidate: bool) -> color_eyre::Result<()> {
|
||||
// Make adjustments to the border
|
||||
let mut rect = *rect;
|
||||
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst));
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
|
||||
|
||||
// Store the border rect so that it can be used by the callback
|
||||
{
|
||||
let mut rects = RECT_STATE.lock();
|
||||
rects.insert(self.hwnd, rect);
|
||||
}
|
||||
|
||||
// Update the position of the border if required
|
||||
if !WindowsApi::window_rect(self.hwnd())?.eq(&rect) {
|
||||
WindowsApi::set_border_pos(self.hwnd(), &rect, HWND((*Z_ORDER.lock()).into()))?;
|
||||
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.
|
||||
self.invalidate();
|
||||
if should_invalidate {
|
||||
self.invalidate();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -156,63 +159,71 @@ impl Border {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_PAINT => {
|
||||
let rects = RECT_STATE.lock();
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
|
||||
// With the rect that we stored in Self::update
|
||||
if let Some(rect) = rects.get(&window.0).copied() {
|
||||
// Grab the focus kind for this border
|
||||
let focus_kind = {
|
||||
FOCUS_STATE
|
||||
.lock()
|
||||
.get(&window.0)
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused)
|
||||
};
|
||||
// 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)
|
||||
};
|
||||
|
||||
// Set up the brush to draw the border
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
let hpen = CreatePen(
|
||||
PS_SOLID | PS_INSIDEFRAME,
|
||||
BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
COLORREF(match focus_kind {
|
||||
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Stack => STACK.load(Ordering::SeqCst),
|
||||
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
|
||||
}),
|
||||
);
|
||||
// Set up the brush to draw the border
|
||||
let hpen = CreatePen(
|
||||
PS_SOLID | PS_INSIDEFRAME,
|
||||
BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
COLORREF(window_kind_colour(window_kind)),
|
||||
);
|
||||
|
||||
let hbrush = WindowsApi::create_solid_brush(0);
|
||||
let hbrush = WindowsApi::create_solid_brush(0);
|
||||
|
||||
// Draw the border
|
||||
SelectObject(hdc, hpen);
|
||||
SelectObject(hdc, hbrush);
|
||||
// TODO(raggi): this is approximately the correct curvature for
|
||||
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
|
||||
// often the bottom right has a different shape. Furthermore if
|
||||
// the window was made with DWMWCP_ROUNDSMALL then this is the
|
||||
// wrong size. In the future we should read the DWM properties
|
||||
// of windows and attempt to match appropriately.
|
||||
match *STYLE.lock() {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
} else {
|
||||
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => {
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
}
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hpen);
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hbrush);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("could not get border rect: {}", error.to_string())
|
||||
}
|
||||
EndPaint(window, &ps);
|
||||
ValidateRect(window, None);
|
||||
}
|
||||
|
||||
// TODO: error handling
|
||||
let _ = EndPaint(window, &ps);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
|
||||
@@ -2,10 +2,21 @@
|
||||
|
||||
mod border;
|
||||
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::WindowKind;
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::Colour;
|
||||
use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
use border::Border;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
use komorebi_core::BorderStyle;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use schemars::JsonSchema;
|
||||
@@ -16,30 +27,21 @@ use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::Colour;
|
||||
use crate::Rect;
|
||||
use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
use border::Border;
|
||||
use komorebi_core::WindowKind;
|
||||
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
|
||||
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
|
||||
|
||||
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref Z_ORDER: Arc<Mutex<ZOrder>> = Arc::new(Mutex::new(ZOrder::Bottom));
|
||||
pub static ref STYLE: Arc<Mutex<BorderStyle>> = Arc::new(Mutex::new(BorderStyle::System));
|
||||
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
|
||||
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
|
||||
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
|
||||
AtomicCell::new(BorderImplementation::Komorebi);
|
||||
pub static ref FOCUSED: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245))));
|
||||
pub static ref UNFOCUSED: AtomicU32 =
|
||||
@@ -47,31 +49,38 @@ lazy_static! {
|
||||
pub static ref MONOCLE: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(255, 51, 153))));
|
||||
pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(0, 165, 66))));
|
||||
pub static ref FLOATING: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(245, 245, 165))));
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
|
||||
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
|
||||
static ref RECT_STATE: Mutex<HashMap<isize, Rect>> = Mutex::new(HashMap::new());
|
||||
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub struct Notification;
|
||||
pub struct Notification(pub Option<isize>);
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(crossbeam_channel::unbounded)
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(50))
|
||||
}
|
||||
|
||||
pub fn event_tx() -> Sender<Notification> {
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
pub fn event_rx() -> Receiver<Notification> {
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification(hwnd: Option<isize>) {
|
||||
if event_tx().try_send(Notification(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
tracing::info!(
|
||||
@@ -84,7 +93,6 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
RECT_STATE.lock().clear();
|
||||
BORDERS_MONITORS.lock().clear();
|
||||
FOCUS_STATE.lock().clear();
|
||||
|
||||
@@ -106,6 +114,16 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
||||
match focus_kind {
|
||||
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Stack => STACK.load(Ordering::SeqCst),
|
||||
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
|
||||
WindowKind::Floating => FLOATING.load(Ordering::SeqCst),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
@@ -122,211 +140,57 @@ 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();
|
||||
let mut instant: Option<Instant> = None;
|
||||
event_tx().send(Notification)?;
|
||||
event_tx().send(Notification(None))?;
|
||||
|
||||
'receiver: for _ in receiver {
|
||||
if let Some(instant) = instant {
|
||||
if instant.elapsed().lt(&Duration::from_millis(50)) {
|
||||
continue 'receiver;
|
||||
}
|
||||
}
|
||||
|
||||
instant = Some(Instant::now());
|
||||
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
let mut borders_monitors = BORDERS_MONITORS.lock();
|
||||
let mut previous_snapshot = Ring::default();
|
||||
let mut previous_pending_move_op = None;
|
||||
let mut previous_is_paused = false;
|
||||
let mut previous_notification: Option<Notification> = None;
|
||||
|
||||
'receiver: for notification in receiver {
|
||||
// Check the wm state every time we receive a notification
|
||||
let state = wm.lock();
|
||||
|
||||
// If borders are disabled
|
||||
if !BORDER_ENABLED.load_consume()
|
||||
// Or if the wm is paused
|
||||
|| state.is_paused
|
||||
// Or if we are handling an alt-tab across workspaces
|
||||
|| ALT_TAB_HWND.load().is_some()
|
||||
{
|
||||
// Destroy the borders we know about
|
||||
for (_, border) in borders.iter() {
|
||||
border.destroy()?;
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
let is_paused = state.is_paused;
|
||||
let focused_monitor_idx = state.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
state.monitors.elements()[focused_monitor_idx].focused_workspace_idx();
|
||||
let monitors = state.monitors.clone();
|
||||
let pending_move_op = state.pending_move_op;
|
||||
let floating_window_hwnds = state.monitors.elements()[focused_monitor_idx].workspaces()
|
||||
[focused_workspace_idx]
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
'monitors: for (monitor_idx, m) in state.monitors.elements().iter().enumerate() {
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Workspaces with tiling disabled don't have borders
|
||||
if !ws.tile() {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let border = match borders.entry(monocle.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(monocle.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'receiver;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(
|
||||
border.hwnd,
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
match IMPLEMENTATION.load() {
|
||||
BorderImplementation::Windows => {
|
||||
'monitors: for (monitor_idx, m) in monitors.elements().iter().enumerate() {
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let window_kind = if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
|
||||
border.update(&rect)?;
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
let mut to_remove = vec![];
|
||||
for (id, b) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
|
||||
&& border_hwnd != b.hwnd
|
||||
{
|
||||
b.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let is_maximized = WindowsApi::is_zoomed(HWND(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
));
|
||||
|
||||
if is_maximized {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx {
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Destroy any borders not associated with the focused workspace
|
||||
let container_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
.map(|c| c.id().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
|
||||
&& !container_ids.contains(id)
|
||||
{
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
// Update border when moving or resizing with mouse
|
||||
if state.pending_move_op.is_some() && idx == ws.focused_container_idx() {
|
||||
let restore_z_order = *Z_ORDER.lock();
|
||||
*Z_ORDER.lock() = ZOrder::TopMost;
|
||||
|
||||
let mut rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
|
||||
while WindowsApi::lbutton_is_pressed() {
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(c.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'receiver;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
monocle
|
||||
.focused_window()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
|
||||
if rect != new_rect {
|
||||
rect = new_rect;
|
||||
border.update(&rect)?;
|
||||
}
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
*Z_ORDER.lock() = restore_z_order;
|
||||
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
// Get the border entry for this container from the map or create one
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(c.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'receiver;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(c.id().clone(), monitor_idx);
|
||||
|
||||
// Update the focused state for all containers on this workspace
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(
|
||||
border.hwnd,
|
||||
if idx != ws.focused_container_idx()
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
let window_kind = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
@@ -334,18 +198,396 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
WindowKind::Single
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
c.focused_window()
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BorderImplementation::Komorebi => {
|
||||
let mut should_process_notification = true;
|
||||
|
||||
if monitors == previous_snapshot
|
||||
// handle the window dragging edge case
|
||||
&& pending_move_op == previous_pending_move_op
|
||||
{
|
||||
should_process_notification = false;
|
||||
}
|
||||
|
||||
// handle the pause edge case
|
||||
if is_paused && !previous_is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the unpause edge case
|
||||
if previous_is_paused && !is_paused {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// handle the retile edge case
|
||||
if !should_process_notification && BORDER_STATE.lock().is_empty() {
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// when we switch focus to a floating window
|
||||
if !should_process_notification
|
||||
&& floating_window_hwnds.contains(¬ification.0.unwrap_or_default())
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
if let Some(ref previous) = previous_notification {
|
||||
if previous.0.unwrap_or_default() != notification.0.unwrap_or_default() {
|
||||
should_process_notification = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
let mut borders_monitors = BORDERS_MONITORS.lock();
|
||||
|
||||
// If borders are disabled
|
||||
if !BORDER_ENABLED.load_consume()
|
||||
// Or if they are temporarily disabled
|
||||
|| BORDER_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
|
||||
// Or if the wm is paused
|
||||
|| is_paused
|
||||
// Or if we are handling an alt-tab across workspaces
|
||||
|| ALT_TAB_HWND.load().is_some()
|
||||
{
|
||||
// Destroy the borders we know about
|
||||
for (_, border) in borders.iter() {
|
||||
border.destroy()?;
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd(),
|
||||
)?;
|
||||
borders.clear();
|
||||
|
||||
border.update(&rect)?;
|
||||
previous_is_paused = is_paused;
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
'monitors: for (monitor_idx, m) in monitors.elements().iter().enumerate() {
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Workspaces with tiling disabled don't have borders
|
||||
if !ws.tile() {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default()
|
||||
== monitor_idx
|
||||
{
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
let border = match borders.entry(monocle.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(monocle.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(
|
||||
border.hwnd,
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
border.update(&rect, true)?;
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
let mut to_remove = vec![];
|
||||
for (id, b) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default()
|
||||
== monitor_idx
|
||||
&& border_hwnd != b.hwnd
|
||||
{
|
||||
b.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let is_maximized = WindowsApi::is_zoomed(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
);
|
||||
|
||||
if is_maximized {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default()
|
||||
== monitor_idx
|
||||
{
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Destroy any borders not associated with the focused workspace
|
||||
let mut container_and_floating_window_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
.map(|c| c.id().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for w in ws.floating_windows() {
|
||||
container_and_floating_window_ids.push(w.hwnd.to_string());
|
||||
}
|
||||
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
|
||||
&& !container_and_floating_window_ids.contains(id)
|
||||
{
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
borders.remove(id);
|
||||
}
|
||||
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
let hwnd = c.focused_window().copied().unwrap_or_default().hwnd;
|
||||
let notification_hwnd = notification.0.unwrap_or_default();
|
||||
|
||||
// Update border when moving or resizing with mouse
|
||||
if pending_move_op.is_some()
|
||||
&& idx == ws.focused_container_idx()
|
||||
&& hwnd == notification_hwnd
|
||||
{
|
||||
let restore_z_order = Z_ORDER.load();
|
||||
Z_ORDER.store(ZOrder::TopMost);
|
||||
|
||||
let mut rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
while WindowsApi::lbutton_is_pressed() {
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(c.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
if rect != new_rect {
|
||||
rect = new_rect;
|
||||
border.update(&rect, true)?;
|
||||
}
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Get the border entry for this container from the map or create one
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(c.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(c.id().clone(), monitor_idx);
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
|
||||
let new_focus_state = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
} else if c.windows().len() > 1 {
|
||||
WindowKind::Stack
|
||||
} else {
|
||||
WindowKind::Single
|
||||
};
|
||||
|
||||
// Update the focused state for all containers on this workspace
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
last_focus_state = focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
let should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
let border = match borders.entry(window.hwnd.to_string()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(&window.hwnd.to_string())
|
||||
{
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
|
||||
|
||||
let mut should_destroy = false;
|
||||
|
||||
if let Some(notification_hwnd) = notification.0 {
|
||||
if notification_hwnd != window.hwnd {
|
||||
should_destroy = true;
|
||||
}
|
||||
}
|
||||
|
||||
if WindowsApi::foreground_window().unwrap_or_default()
|
||||
!= window.hwnd
|
||||
{
|
||||
should_destroy = true;
|
||||
}
|
||||
|
||||
if should_destroy {
|
||||
border.destroy()?;
|
||||
borders.remove(&window.hwnd.to_string());
|
||||
borders_monitors.remove(&window.hwnd.to_string());
|
||||
continue 'windows;
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
let new_focus_state = WindowKind::Floating;
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
last_focus_state =
|
||||
focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
|
||||
let should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previous_snapshot = monitors;
|
||||
previous_pending_move_op = pending_move_op;
|
||||
previous_is_paused = is_paused;
|
||||
previous_notification = Some(notification);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use hex_color::HexColor;
|
||||
use komorebi_themes::Color32;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::InstanceType;
|
||||
use schemars::schema::Schema;
|
||||
@@ -28,6 +29,16 @@ impl From<u32> for Colour {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color32> for Colour {
|
||||
fn from(value: Color32) -> Self {
|
||||
Colour::Rgb(Rgb::new(
|
||||
value.r() as u32,
|
||||
value.g() as u32,
|
||||
value.b() as u32,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct Hex(HexColor);
|
||||
|
||||
@@ -39,6 +50,7 @@ impl JsonSchema for Hex {
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
format: Some("color-hex".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
|
||||
@@ -10,13 +10,13 @@ use interfaces::IServiceProvider;
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use windows::core::Interface;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::System::Com::CoCreateInstance;
|
||||
use windows::Win32::System::Com::CoInitializeEx;
|
||||
use windows::Win32::System::Com::CoUninitialize;
|
||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||
use windows::Win32::System::Com::COINIT_APARTMENTTHREADED;
|
||||
use windows_core::Interface;
|
||||
|
||||
struct ComInit();
|
||||
|
||||
@@ -77,7 +77,7 @@ pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
|
||||
if view_collection.get_view_for_hwnd(hwnd, &mut view).is_err() {
|
||||
tracing::error!(
|
||||
"could not get view for hwnd {} due to os error: {}",
|
||||
hwnd.0,
|
||||
hwnd.0 as isize,
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
@@ -85,14 +85,14 @@ pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
|
||||
|
||||
view.map_or_else(
|
||||
|| {
|
||||
tracing::error!("no view was found for {}", hwnd.0,);
|
||||
tracing::error!("no view was found for {}", hwnd.0 as isize);
|
||||
},
|
||||
|view| {
|
||||
unsafe {
|
||||
if view.set_cloak(cloak_type, flags).is_err() {
|
||||
tracing::error!(
|
||||
"could not change the cloaking status for hwnd {} due to os error: {}",
|
||||
hwnd.0,
|
||||
hwnd.0 as isize,
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde::Serialize;
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, JsonSchema)]
|
||||
pub struct Container {
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
@@ -27,12 +27,6 @@ impl Default for Container {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Container {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn hide(&self, omit: Option<isize>) {
|
||||
for window in self.windows().iter().rev() {
|
||||
@@ -104,11 +98,7 @@ impl Container {
|
||||
|
||||
pub fn remove_window_by_idx(&mut self, idx: usize) -> Option<Window> {
|
||||
let window = self.windows_mut().remove(idx);
|
||||
|
||||
if idx != 0 {
|
||||
self.focus_window(idx - 1);
|
||||
};
|
||||
|
||||
self.focus_window(idx.saturating_sub(1));
|
||||
window
|
||||
}
|
||||
|
||||
@@ -119,7 +109,14 @@ impl Container {
|
||||
|
||||
pub fn add_window(&mut self, window: Window) {
|
||||
self.windows_mut().push_back(window);
|
||||
self.focus_window(self.windows().len() - 1);
|
||||
self.focus_window(self.windows().len().saturating_sub(1));
|
||||
let focused_window_idx = self.focused_window_idx();
|
||||
|
||||
for (i, window) in self.windows().iter().enumerate() {
|
||||
if i != focused_window_idx {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
|
||||
42
komorebi/src/core/animation.rs
Normal file
42
komorebi/src/core/animation.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use clap::ValueEnum;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
pub enum AnimationStyle {
|
||||
Linear,
|
||||
EaseInSine,
|
||||
EaseOutSine,
|
||||
EaseInOutSine,
|
||||
EaseInQuad,
|
||||
EaseOutQuad,
|
||||
EaseInOutQuad,
|
||||
EaseInCubic,
|
||||
EaseInOutCubic,
|
||||
EaseInQuart,
|
||||
EaseOutQuart,
|
||||
EaseInOutQuart,
|
||||
EaseInQuint,
|
||||
EaseOutQuint,
|
||||
EaseInOutQuint,
|
||||
EaseInExpo,
|
||||
EaseOutExpo,
|
||||
EaseInOutExpo,
|
||||
EaseInCirc,
|
||||
EaseOutCirc,
|
||||
EaseInOutCirc,
|
||||
EaseInBack,
|
||||
EaseOutBack,
|
||||
EaseInOutBack,
|
||||
EaseInElastic,
|
||||
EaseOutElastic,
|
||||
EaseInOutElastic,
|
||||
EaseInBounce,
|
||||
EaseOutBounce,
|
||||
EaseInOutBounce,
|
||||
}
|
||||
@@ -7,12 +7,12 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::custom_layout::Column;
|
||||
use crate::custom_layout::ColumnSplit;
|
||||
use crate::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Rect;
|
||||
use super::custom_layout::Column;
|
||||
use super::custom_layout::ColumnSplit;
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Rect;
|
||||
|
||||
pub trait Arrangement {
|
||||
fn calculate(
|
||||
@@ -604,7 +604,16 @@ impl Arrangement for CustomLayout {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
pub enum Axis {
|
||||
Horizontal,
|
||||
135
komorebi/src/core/asc.rs
Normal file
135
komorebi/src/core/asc.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use crate::config_generation::ApplicationConfiguration;
|
||||
use crate::config_generation::ApplicationOptions;
|
||||
use crate::config_generation::MatchingRule;
|
||||
use color_eyre::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApplicationSpecificConfiguration(pub BTreeMap<String, AscApplicationRulesOrSchema>);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum AscApplicationRulesOrSchema {
|
||||
AscApplicationRules(AscApplicationRules),
|
||||
Schema(String),
|
||||
}
|
||||
|
||||
impl Deref for ApplicationSpecificConfiguration {
|
||||
type Target = BTreeMap<String, AscApplicationRulesOrSchema>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ApplicationSpecificConfiguration {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationSpecificConfiguration {
|
||||
pub fn load(pathbuf: &PathBuf) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(pathbuf)?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
}
|
||||
|
||||
pub fn format(pathbuf: &PathBuf) -> Result<String> {
|
||||
Ok(serde_json::to_string_pretty(&Self::load(pathbuf)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rules that determine how an application is handled
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AscApplicationRules {
|
||||
/// Rules to ignore specific windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore: Option<Vec<MatchingRule>>,
|
||||
/// Rules to forcibly manage specific windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manage: Option<Vec<MatchingRule>>,
|
||||
/// Rules to manage specific windows as floating windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub floating: Option<Vec<MatchingRule>>,
|
||||
/// Rules to ignore specific windows from the transparency feature
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transparency_ignore: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which minimize to the tray or have multiple windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_and_multi_window: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which have the `WS_EX_LAYERED` Extended Window Style
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layered: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which send the `EVENT_OBJECT_NAMECHANGE` event on launch
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_name_change: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which are slow to send initial event notifications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slow_application: Option<Vec<MatchingRule>>,
|
||||
}
|
||||
|
||||
impl From<Vec<ApplicationConfiguration>> for ApplicationSpecificConfiguration {
|
||||
fn from(value: Vec<ApplicationConfiguration>) -> Self {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
for entry in &value {
|
||||
let key = entry.name.clone();
|
||||
let mut rules = AscApplicationRules {
|
||||
ignore: None,
|
||||
manage: None,
|
||||
floating: None,
|
||||
transparency_ignore: None,
|
||||
tray_and_multi_window: None,
|
||||
layered: None,
|
||||
object_name_change: None,
|
||||
slow_application: None,
|
||||
};
|
||||
|
||||
rules.ignore = entry.ignore_identifiers.clone();
|
||||
|
||||
if let Some(options) = &entry.options {
|
||||
for opt in options {
|
||||
match opt {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
rules.object_name_change =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
rules.layered =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
rules.tray_and_multi_window =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
rules.manage =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rules.ignore.is_some()
|
||||
|| rules.manage.is_some()
|
||||
|| rules.floating.is_some()
|
||||
|| rules.transparency_ignore.is_some()
|
||||
|| rules.tray_and_multi_window.is_some()
|
||||
|| rules.layered.is_some()
|
||||
|| rules.object_name_change.is_some()
|
||||
|| rules.slow_application.is_some()
|
||||
{
|
||||
map.insert(key, AscApplicationRulesOrSchema::AscApplicationRules(rules));
|
||||
}
|
||||
}
|
||||
|
||||
Self(map)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::ApplicationIdentifier;
|
||||
use super::ApplicationIdentifier;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
@@ -59,6 +59,14 @@ pub enum MatchingRule {
|
||||
Composite(Vec<IdWithIdentifier>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WorkspaceMatchingRule {
|
||||
pub monitor_index: usize,
|
||||
pub workspace_index: usize,
|
||||
pub matching_rule: MatchingRule,
|
||||
pub initial_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct IdWithIdentifier {
|
||||
pub kind: ApplicationIdentifier,
|
||||
@@ -108,7 +116,8 @@ pub struct ApplicationConfiguration {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub options: Option<Vec<ApplicationOptions>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_identifiers: Option<Vec<MatchingRule>>,
|
||||
#[serde(alias = "float_identifiers")]
|
||||
pub ignore_identifiers: Option<Vec<MatchingRule>>,
|
||||
}
|
||||
|
||||
impl ApplicationConfiguration {
|
||||
@@ -179,7 +188,7 @@ impl ApplicationConfigurationGenerator {
|
||||
|
||||
let mut lines = vec![String::from("# Generated by komorebic.exe"), String::new()];
|
||||
|
||||
let mut float_rules = vec![];
|
||||
let mut ignore_rules = vec![];
|
||||
|
||||
for app in cfgen {
|
||||
lines.push(format!("# {}", app.name));
|
||||
@@ -193,15 +202,15 @@ impl ApplicationConfigurationGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(float_identifiers) = app.float_identifiers {
|
||||
for matching_rule in float_identifiers {
|
||||
if let Some(ignore_identifiers) = app.ignore_identifiers {
|
||||
for matching_rule in ignore_identifiers {
|
||||
if let MatchingRule::Simple(float) = matching_rule {
|
||||
let float_rule =
|
||||
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
|
||||
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
if !ignore_rules.contains(&float_rule) {
|
||||
ignore_rules.push(float_rule.clone());
|
||||
|
||||
// if let Some(comment) = float.comment {
|
||||
// lines.push(format!("# {comment}"));
|
||||
@@ -230,7 +239,7 @@ impl ApplicationConfigurationGenerator {
|
||||
|
||||
let mut lines = vec![String::from("; Generated by komorebic.exe"), String::new()];
|
||||
|
||||
let mut float_rules = vec![];
|
||||
let mut ignore_rules = vec![];
|
||||
|
||||
for app in cfgen {
|
||||
lines.push(format!("; {}", app.name));
|
||||
@@ -244,8 +253,8 @@ impl ApplicationConfigurationGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(float_identifiers) = app.float_identifiers {
|
||||
for matching_rule in float_identifiers {
|
||||
if let Some(ignore_identifiers) = app.ignore_identifiers {
|
||||
for matching_rule in ignore_identifiers {
|
||||
if let MatchingRule::Simple(float) = matching_rule {
|
||||
let float_rule = format!(
|
||||
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
|
||||
@@ -253,8 +262,8 @@ impl ApplicationConfigurationGenerator {
|
||||
);
|
||||
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
if !ignore_rules.contains(&float_rule) {
|
||||
ignore_rules.push(float_rule.clone());
|
||||
|
||||
// if let Some(comment) = float.comment {
|
||||
// lines.push(format!("; {comment}"));
|
||||
@@ -12,9 +12,9 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Rect;
|
||||
use super::Rect;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct CustomLayout(Vec<Column>);
|
||||
|
||||
impl Deref for CustomLayout {
|
||||
@@ -250,7 +250,7 @@ impl CustomLayout {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "column", content = "configuration")]
|
||||
pub enum Column {
|
||||
Primary(Option<ColumnWidth>),
|
||||
@@ -258,18 +258,18 @@ pub enum Column {
|
||||
Tertiary(ColumnSplit),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum ColumnWidth {
|
||||
WidthPercentage(f32),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum ColumnSplit {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum ColumnSplitWithCapacity {
|
||||
Horizontal(usize),
|
||||
Vertical(usize),
|
||||
@@ -5,9 +5,9 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::OperationDirection;
|
||||
use crate::Rect;
|
||||
use crate::Sizing;
|
||||
use super::OperationDirection;
|
||||
use super::Rect;
|
||||
use super::Sizing;
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
@@ -35,6 +35,37 @@ pub enum DefaultLayout {
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
Self::UltrawideVerticalStack | Self::RightMainVerticalStack => match len {
|
||||
n if n > 1 => 1,
|
||||
_ => 0,
|
||||
},
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::HorizontalStack
|
||||
| DefaultLayout::Grid => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rightmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::HorizontalStack
|
||||
| DefaultLayout::Grid => len.saturating_sub(1),
|
||||
DefaultLayout::UltrawideVerticalStack => match len {
|
||||
2 => 0,
|
||||
_ => len.saturating_sub(1),
|
||||
},
|
||||
DefaultLayout::RightMainVerticalStack => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
|
||||
pub fn resize(
|
||||
@@ -164,14 +195,14 @@ impl DefaultLayout {
|
||||
#[must_use]
|
||||
pub const fn cycle_previous(self) -> Self {
|
||||
match self {
|
||||
Self::BSP => Self::UltrawideVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::Rows,
|
||||
Self::Rows => Self::Columns,
|
||||
Self::Columns => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
Self::Columns => Self::BSP,
|
||||
Self::BSP => Self::RightMainVerticalStack,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::custom_layout::Column;
|
||||
use crate::custom_layout::ColumnSplit;
|
||||
use crate::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::custom_layout::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::OperationDirection;
|
||||
use super::custom_layout::Column;
|
||||
use super::custom_layout::ColumnSplit;
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use super::custom_layout::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::OperationDirection;
|
||||
|
||||
pub trait Direction {
|
||||
fn index_in_direction(
|
||||
@@ -90,9 +90,13 @@ impl Direction for DefaultLayout {
|
||||
idx: usize,
|
||||
count: usize,
|
||||
) -> bool {
|
||||
if count < 2 {
|
||||
return false;
|
||||
}
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Up => match self {
|
||||
Self::BSP => count > 2 && idx != 0 && idx != 1,
|
||||
Self::BSP => idx != 0 && idx != 1,
|
||||
Self::Columns => false,
|
||||
Self::Rows | Self::HorizontalStack => idx != 0,
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
||||
@@ -100,7 +104,7 @@ impl Direction for DefaultLayout {
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
OperationDirection::Down => match self {
|
||||
Self::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
|
||||
Self::BSP => idx != count - 1 && idx % 2 != 0,
|
||||
Self::Columns => false,
|
||||
Self::Rows => idx != count - 1,
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
|
||||
@@ -109,23 +113,22 @@ impl Direction for DefaultLayout {
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
Self::BSP => count > 1 && idx != 0,
|
||||
Self::BSP => idx != 0,
|
||||
Self::Columns | Self::VerticalStack => idx != 0,
|
||||
Self::RightMainVerticalStack => idx == 0,
|
||||
Self::Rows => false,
|
||||
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => count > 1 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx != 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
Self::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
|
||||
Self::BSP => idx % 2 == 0 && idx != count - 1,
|
||||
Self::Columns => idx != count - 1,
|
||||
Self::Rows => false,
|
||||
Self::VerticalStack => idx == 0,
|
||||
Self::RightMainVerticalStack => idx != 0,
|
||||
Self::HorizontalStack => idx != 0 && idx != count - 1,
|
||||
Self::UltrawideVerticalStack => match count {
|
||||
0 | 1 => false,
|
||||
2 => idx != 0,
|
||||
_ => idx < 2,
|
||||
},
|
||||
@@ -2,12 +2,12 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Arrangement;
|
||||
use crate::CustomLayout;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Direction;
|
||||
use super::Arrangement;
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Direction;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum Layout {
|
||||
Default(DefaultLayout),
|
||||
Custom(CustomLayout),
|
||||
@@ -1,5 +1,5 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self)]
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -14,6 +14,7 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
pub use custom_layout::CustomLayout;
|
||||
@@ -24,7 +25,9 @@ pub use layout::Layout;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
pub mod arrangement;
|
||||
pub mod asc;
|
||||
pub mod config_generation;
|
||||
pub mod custom_layout;
|
||||
pub mod cycle_direction;
|
||||
@@ -43,10 +46,13 @@ pub enum SocketMessage {
|
||||
CycleFocusWindow(CycleDirection),
|
||||
CycleMoveWindow(CycleDirection),
|
||||
StackWindow(OperationDirection),
|
||||
ResizeWindowEdge(OperationDirection, Sizing),
|
||||
ResizeWindowAxis(Axis, Sizing),
|
||||
UnstackWindow,
|
||||
CycleStack(CycleDirection),
|
||||
FocusStackWindow(usize),
|
||||
StackAll,
|
||||
UnstackAll,
|
||||
ResizeWindowEdge(OperationDirection, Sizing),
|
||||
ResizeWindowAxis(Axis, Sizing),
|
||||
MoveContainerToMonitorNumber(usize),
|
||||
CycleMoveContainerToMonitor(CycleDirection),
|
||||
MoveContainerToWorkspaceNumber(usize),
|
||||
@@ -72,6 +78,7 @@ pub enum SocketMessage {
|
||||
ToggleMonocle,
|
||||
ToggleMaximize,
|
||||
ToggleWindowContainerBehaviour,
|
||||
ToggleFloatOverride,
|
||||
WindowHidingBehaviour(HidingBehaviour),
|
||||
ToggleCrossMonitorMoveBehaviour,
|
||||
CrossMonitorMoveBehaviour(MoveBehaviour),
|
||||
@@ -85,6 +92,8 @@ pub enum SocketMessage {
|
||||
CycleLayout(CycleDirection),
|
||||
ChangeLayoutCustom(PathBuf),
|
||||
FlipLayout(Axis),
|
||||
ToggleWorkspaceWindowContainerBehaviour,
|
||||
ToggleWorkspaceFloatOverride,
|
||||
// Monitor and Workspace Commands
|
||||
MonitorIndexPreference(usize, i32, i32, i32, i32),
|
||||
DisplayIndexPreference(usize, String),
|
||||
@@ -95,6 +104,7 @@ pub enum SocketMessage {
|
||||
Stop,
|
||||
TogglePause,
|
||||
Retile,
|
||||
RetileWithResizeDimensions,
|
||||
QuickSave,
|
||||
QuickLoad,
|
||||
Save(PathBuf),
|
||||
@@ -128,10 +138,15 @@ pub enum SocketMessage {
|
||||
ClearNamedWorkspaceLayoutRules(String),
|
||||
// Configuration
|
||||
ReloadConfiguration,
|
||||
ReplaceConfiguration(PathBuf),
|
||||
ReloadStaticConfiguration(PathBuf),
|
||||
WatchConfiguration(bool),
|
||||
CompleteConfiguration,
|
||||
AltFocusHack(bool),
|
||||
Animation(bool),
|
||||
AnimationDuration(u64),
|
||||
AnimationFps(u64),
|
||||
AnimationStyle(AnimationStyle),
|
||||
#[serde(alias = "ActiveWindowBorder")]
|
||||
Border(bool),
|
||||
#[serde(alias = "ActiveWindowBorderColour")]
|
||||
@@ -140,6 +155,10 @@ pub enum SocketMessage {
|
||||
BorderStyle(BorderStyle),
|
||||
BorderWidth(i32),
|
||||
BorderOffset(i32),
|
||||
BorderImplementation(BorderImplementation),
|
||||
Transparency(bool),
|
||||
ToggleTransparency,
|
||||
TransparencyAlpha(u8),
|
||||
InvisibleBorders(Rect),
|
||||
StackbarMode(StackbarMode),
|
||||
StackbarLabel(StackbarLabel),
|
||||
@@ -148,6 +167,8 @@ pub enum SocketMessage {
|
||||
StackbarBackgroundColour(u32, u32, u32),
|
||||
StackbarHeight(i32),
|
||||
StackbarTabWidth(i32),
|
||||
StackbarFontSize(i32),
|
||||
StackbarFontFamily(Option<String>),
|
||||
WorkAreaOffset(Rect),
|
||||
MonitorWorkAreaOffset(usize, Rect),
|
||||
ResizeDelta(i32),
|
||||
@@ -155,7 +176,11 @@ pub enum SocketMessage {
|
||||
InitialNamedWorkspaceRule(ApplicationIdentifier, String, String),
|
||||
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
NamedWorkspaceRule(ApplicationIdentifier, String, String),
|
||||
FloatRule(ApplicationIdentifier, String),
|
||||
ClearWorkspaceRules(usize, usize),
|
||||
ClearNamedWorkspaceRules(String),
|
||||
ClearAllWorkspaceRules,
|
||||
#[serde(alias = "FloatRule")]
|
||||
IgnoreRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
IdentifyObjectNameChangeApplication(ApplicationIdentifier, String),
|
||||
IdentifyTrayApplication(ApplicationIdentifier, String),
|
||||
@@ -164,6 +189,7 @@ pub enum SocketMessage {
|
||||
State,
|
||||
GlobalState,
|
||||
VisibleWindows,
|
||||
MonitorInformation,
|
||||
Query(StateQuery),
|
||||
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
|
||||
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
|
||||
@@ -172,6 +198,7 @@ pub enum SocketMessage {
|
||||
RemoveTitleBar(ApplicationIdentifier, String),
|
||||
ToggleTitleBars,
|
||||
AddSubscriberSocket(String),
|
||||
AddSubscriberSocketWithOptions(String, SubscribeOptions),
|
||||
RemoveSubscriberSocket(String),
|
||||
AddSubscriberPipe(String),
|
||||
RemoveSubscriberPipe(String),
|
||||
@@ -197,6 +224,12 @@ impl FromStr for SocketMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct SubscribeOptions {
|
||||
/// Only emit notifications when the window manager state has changed
|
||||
pub filter_state_changes: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum StackbarMode {
|
||||
Always,
|
||||
@@ -214,7 +247,17 @@ pub enum StackbarLabel {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
|
||||
Default,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Display,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
ValueEnum,
|
||||
)]
|
||||
pub enum BorderStyle {
|
||||
#[default]
|
||||
@@ -227,13 +270,44 @@ pub enum BorderStyle {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Default,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Display,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
ValueEnum,
|
||||
)]
|
||||
pub enum BorderImplementation {
|
||||
#[default]
|
||||
/// Use the adjustable komorebi border implementation
|
||||
Komorebi,
|
||||
/// Use the thin Windows accent border implementation
|
||||
Windows,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
pub enum WindowKind {
|
||||
Single,
|
||||
Stack,
|
||||
Monocle,
|
||||
Unfocused,
|
||||
Floating,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -271,7 +345,16 @@ pub enum ApplicationIdentifier {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum FocusFollowsMouseImplementation {
|
||||
/// A custom FFM implementation (slightly more CPU-intensive)
|
||||
@@ -280,18 +363,48 @@ pub enum FocusFollowsMouseImplementation {
|
||||
Windows,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct WindowManagementBehaviour {
|
||||
/// The current WindowContainerBehaviour to be used
|
||||
pub current_behaviour: WindowContainerBehaviour,
|
||||
/// Override of `current_behaviour` to open new windows as floating windows
|
||||
/// that can be later toggled to tiled, when false it will default to
|
||||
/// `current_behaviour` again.
|
||||
pub float_override: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
pub enum WindowContainerBehaviour {
|
||||
/// Create a new container for each new window
|
||||
#[default]
|
||||
Create,
|
||||
/// Append new windows to the focused window container
|
||||
Append,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum MoveBehaviour {
|
||||
/// Swap the window container with the window container at the edge of the adjacent monitor
|
||||
@@ -302,6 +415,16 @@ pub enum MoveBehaviour {
|
||||
NoOp,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
pub enum CrossBoundaryBehaviour {
|
||||
/// Attempt to perform actions across a workspace boundary
|
||||
Workspace,
|
||||
/// Attempt to perform actions across a monitor boundary
|
||||
Monitor,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
)]
|
||||
@@ -310,12 +433,21 @@ pub enum HidingBehaviour {
|
||||
Hide,
|
||||
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
Minimize,
|
||||
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
|
||||
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||
Cloak,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum OperationBehaviour {
|
||||
/// Process komorebic commands on temporarily unmanaged/floated windows
|
||||
@@ -7,8 +7,8 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::direction::Direction;
|
||||
use crate::Axis;
|
||||
use super::direction::Direction;
|
||||
use super::Axis;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
@@ -84,4 +84,14 @@ impl Rect {
|
||||
bottom: (self.bottom * rect_dpi) / system_dpi,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn rect(&self) -> RECT {
|
||||
RECT {
|
||||
left: self.left,
|
||||
top: self.top,
|
||||
right: self.left + self.right,
|
||||
bottom: self.top + self.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
70
komorebi/src/focus_manager.rs
Normal file
70
komorebi/src/focus_manager.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use parking_lot::Mutex;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::Window;
|
||||
use crate::WindowManager;
|
||||
|
||||
pub struct Notification(isize);
|
||||
|
||||
impl Deref for Notification {
|
||||
type Target = isize;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
// Currently this should only be used for async focus updates, such as
|
||||
// when an animation finishes and we need to focus to set the cursor
|
||||
// position if the user has mouse follows focus enabled
|
||||
pub fn send_notification(hwnd: isize) {
|
||||
if event_tx().try_send(Notification(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
|
||||
for notification in receiver {
|
||||
let mouse_follows_focus = wm.lock().mouse_follows_focus;
|
||||
let _ = Window::from(*notification).focus(mouse_follows_focus);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
pub mod animation;
|
||||
pub mod animation_manager;
|
||||
pub mod border_manager;
|
||||
pub mod com;
|
||||
#[macro_use]
|
||||
pub mod ring;
|
||||
pub mod colour;
|
||||
pub mod container;
|
||||
pub mod core;
|
||||
pub mod focus_manager;
|
||||
pub mod monitor;
|
||||
pub mod monitor_reconciliator;
|
||||
pub mod process_command;
|
||||
@@ -14,6 +20,7 @@ pub mod set_window_position;
|
||||
pub mod stackbar_manager;
|
||||
pub mod static_config;
|
||||
pub mod styles;
|
||||
pub mod transparency_manager;
|
||||
pub mod window;
|
||||
pub mod window_manager;
|
||||
pub mod window_manager_event;
|
||||
@@ -35,10 +42,14 @@ use std::process::Command;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use animation::*;
|
||||
pub use animation_manager::*;
|
||||
pub use colour::*;
|
||||
pub use core::*;
|
||||
pub use process_command::*;
|
||||
pub use process_event::*;
|
||||
pub use static_config::*;
|
||||
@@ -48,14 +59,11 @@ pub use window_manager_event::*;
|
||||
pub use windows_api::WindowsApi;
|
||||
pub use windows_api::*;
|
||||
|
||||
use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use crate::core::config_generation::WorkspaceMatchingRule;
|
||||
use color_eyre::Result;
|
||||
use komorebi_core::config_generation::IdWithIdentifier;
|
||||
use komorebi_core::config_generation::MatchingRule;
|
||||
use komorebi_core::config_generation::MatchingStrategy;
|
||||
use komorebi_core::ApplicationIdentifier;
|
||||
use komorebi_core::HidingBehaviour;
|
||||
use komorebi_core::Rect;
|
||||
use komorebi_core::SocketMessage;
|
||||
use os_info::Version;
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
@@ -67,8 +75,6 @@ use which::which;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
use winreg::RegKey;
|
||||
|
||||
type WorkspaceRule = (usize, usize, bool);
|
||||
|
||||
lazy_static! {
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref LAYERED_WHITELIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
@@ -123,16 +129,17 @@ lazy_static! {
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
]));
|
||||
static ref TRANSPARENCY_BLACKLIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref DISPLAY_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, String>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref WORKSPACE_RULES: Arc<Mutex<HashMap<String, WorkspaceRule>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref WORKSPACE_MATCHING_RULES: Arc<Mutex<Vec<WorkspaceMatchingRule>>> =
|
||||
Arc::new(Mutex::new(Vec::new()));
|
||||
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
static ref IGNORE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
// mstsc.exe creates these on Windows 11 when a WSL process is launched
|
||||
// https://github.com/LGUG2Z/komorebi/issues/74
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
@@ -144,9 +151,14 @@ lazy_static! {
|
||||
kind: ApplicationIdentifier::Class,
|
||||
id: String::from("IHWindowClass"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("komorebi-bar.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
]));
|
||||
|
||||
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
@@ -154,9 +166,18 @@ lazy_static! {
|
||||
"X410.exe".to_string(),
|
||||
"vcxsrv.exe".to_string(),
|
||||
]));
|
||||
static ref SLOW_APPLICATION_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("firefox.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
}),
|
||||
]));
|
||||
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
|
||||
pub static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref SUBSCRIPTION_SOCKET_OPTIONS: Arc<Mutex<HashMap<String, SubscribeOptions>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
@@ -194,13 +215,18 @@ lazy_static! {
|
||||
)
|
||||
};
|
||||
|
||||
static ref ANIMATION_STYLE: Arc<Mutex<AnimationStyle >> =
|
||||
Arc::new(Mutex::new(AnimationStyle::Linear));
|
||||
|
||||
static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
|
||||
Arc::new(Mutex::new(AnimationManager::new()));
|
||||
|
||||
// Use app-specific titlebar removal options where possible
|
||||
// eg. Windows Terminal, IntelliJ IDEA, Firefox
|
||||
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
@@ -211,6 +237,10 @@ pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
|
||||
pub static ANIMATION_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250);
|
||||
|
||||
pub static SLOW_APPLICATION_COMPENSATION_TIME: AtomicU64 = AtomicU64::new(20);
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
@@ -269,18 +299,34 @@ pub struct Notification {
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
|
||||
let is_subscription_event = matches!(
|
||||
notification.event,
|
||||
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
|
||||
| NotificationEvent::Socket(SocketMessage::AddSubscriberSocketWithOptions(_, _))
|
||||
);
|
||||
|
||||
let notification = &serde_json::to_string(¬ification)?;
|
||||
let mut stale_sockets = vec![];
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
let options = SUBSCRIPTION_SOCKET_OPTIONS.lock();
|
||||
|
||||
for (socket, path) in &mut *sockets {
|
||||
match UnixStream::connect(path) {
|
||||
Ok(mut stream) => {
|
||||
tracing::debug!("pushed notification to subscriber: {socket}");
|
||||
stream.write_all(notification.as_bytes())?;
|
||||
}
|
||||
Err(_) => {
|
||||
stale_sockets.push(socket.clone());
|
||||
let apply_state_filter = (*options)
|
||||
.get(socket)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.filter_state_changes;
|
||||
|
||||
if !apply_state_filter || state_has_been_modified || is_subscription_event {
|
||||
match UnixStream::connect(path) {
|
||||
Ok(mut stream) => {
|
||||
tracing::debug!("pushed notification to subscriber: {socket}");
|
||||
stream.write_all(notification.as_bytes())?;
|
||||
}
|
||||
Err(_) => {
|
||||
stale_sockets.push(socket.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,6 +334,13 @@ pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
for socket in stale_sockets {
|
||||
tracing::warn!("removing stale subscription: {socket}");
|
||||
sockets.remove(&socket);
|
||||
let socket_path = DATA_DIR.join(socket);
|
||||
if let Err(error) = std::fs::remove_file(&socket_path) {
|
||||
tracing::error!(
|
||||
"could not remove stale subscriber socket file at {}: {error}",
|
||||
socket_path.display()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let mut stale_pipes = vec![];
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
|
||||
#![warn(clippy::all)]
|
||||
#![allow(
|
||||
clippy::missing_errors_doc,
|
||||
clippy::redundant_pub_crate,
|
||||
clippy::significant_drop_tightening,
|
||||
clippy::significant_drop_in_scrutinee
|
||||
clippy::significant_drop_in_scrutinee,
|
||||
clippy::doc_markdown
|
||||
)]
|
||||
|
||||
use std::net::Shutdown;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
@@ -19,11 +21,14 @@ use crossbeam_utils::Backoff;
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
use sysinfo::Process;
|
||||
use sysinfo::ProcessesToUpdate;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use komorebi::border_manager;
|
||||
use komorebi::focus_manager;
|
||||
use komorebi::load_configuration;
|
||||
use komorebi::monitor_reconciliator;
|
||||
use komorebi::process_command::listen_for_commands;
|
||||
@@ -33,6 +38,7 @@ use komorebi::process_movement::listen_for_movements;
|
||||
use komorebi::reaper;
|
||||
use komorebi::stackbar_manager;
|
||||
use komorebi::static_config::StaticConfig;
|
||||
use komorebi::transparency_manager;
|
||||
use komorebi::window_manager::WindowManager;
|
||||
use komorebi::windows_api::WindowsApi;
|
||||
use komorebi::winevent_listener;
|
||||
@@ -43,6 +49,8 @@ use komorebi::HOME_DIR;
|
||||
use komorebi::INITIAL_CONFIGURATION_LOADED;
|
||||
use komorebi::SESSION_ID;
|
||||
|
||||
shadow_rs::shadow!(build);
|
||||
|
||||
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
@@ -130,7 +138,7 @@ fn detect_deadlocks() {
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
||||
struct Opts {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(short, long = "ffm")]
|
||||
@@ -160,9 +168,9 @@ fn main() -> Result<()> {
|
||||
SESSION_ID.store(session_id, Ordering::SeqCst);
|
||||
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes();
|
||||
system.refresh_processes(ProcessesToUpdate::All);
|
||||
|
||||
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe").collect();
|
||||
let matched_procs: Vec<&Process> = system.processes_by_name("komorebi.exe".as_ref()).collect();
|
||||
|
||||
if matched_procs.len() > 1 {
|
||||
let mut len = matched_procs.len();
|
||||
@@ -213,6 +221,7 @@ fn main() -> Result<()> {
|
||||
Arc::new(Mutex::new(StaticConfig::preload(
|
||||
config,
|
||||
winevent_listener::event_rx(),
|
||||
None,
|
||||
)?))
|
||||
} else {
|
||||
Arc::new(Mutex::new(WindowManager::new(
|
||||
@@ -257,9 +266,11 @@ fn main() -> Result<()> {
|
||||
|
||||
border_manager::listen_for_notifications(wm.clone());
|
||||
stackbar_manager::listen_for_notifications(wm.clone());
|
||||
transparency_manager::listen_for_notifications(wm.clone());
|
||||
workspace_reconciliator::listen_for_notifications(wm.clone());
|
||||
monitor_reconciliator::listen_for_notifications(wm.clone())?;
|
||||
reaper::watch_for_orphans(wm.clone());
|
||||
focus_manager::listen_for_notifications(wm.clone());
|
||||
|
||||
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
||||
ctrlc::set_handler(move || {
|
||||
@@ -280,5 +291,15 @@ fn main() -> Result<()> {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
}
|
||||
|
||||
let sockets = komorebi::SUBSCRIPTION_SOCKETS.lock();
|
||||
for path in (*sockets).values() {
|
||||
if let Ok(stream) = UnixStream::connect(path) {
|
||||
stream.shutdown(Shutdown::Both)?;
|
||||
}
|
||||
}
|
||||
|
||||
let socket = DATA_DIR.join("komorebi.sock");
|
||||
let _ = std::fs::remove_file(socket);
|
||||
|
||||
std::process::exit(130);
|
||||
}
|
||||
|
||||
@@ -12,14 +12,27 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use komorebi_core::Rect;
|
||||
use crate::core::Rect;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Layout;
|
||||
use crate::OperationDirection;
|
||||
use crate::WindowsApi;
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, JsonSchema,
|
||||
Debug,
|
||||
Clone,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Getters,
|
||||
CopyGetters,
|
||||
MutGetters,
|
||||
Setters,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
pub struct Monitor {
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
@@ -78,6 +91,22 @@ pub fn new(
|
||||
}
|
||||
|
||||
impl Monitor {
|
||||
pub fn placeholder() -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
name: "PLACEHOLDER".to_string(),
|
||||
device: "".to_string(),
|
||||
device_id: "".to_string(),
|
||||
size: Default::default(),
|
||||
work_area_size: Default::default(),
|
||||
work_area_offset: None,
|
||||
window_based_work_area_offset: None,
|
||||
window_based_work_area_offset_limit: 0,
|
||||
workspaces: Default::default(),
|
||||
last_focused_workspace: None,
|
||||
workspace_names: Default::default(),
|
||||
}
|
||||
}
|
||||
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
|
||||
let focused_idx = self.focused_workspace_idx();
|
||||
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
|
||||
@@ -105,7 +134,7 @@ impl Monitor {
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
};
|
||||
|
||||
workspace.add_container(container);
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -118,7 +147,7 @@ impl Monitor {
|
||||
if idx == 0 {
|
||||
self.workspaces_mut().push_back(Workspace::default());
|
||||
} else {
|
||||
self.focus_workspace(idx - 1).ok()?;
|
||||
self.focus_workspace(idx.saturating_sub(1)).ok()?;
|
||||
};
|
||||
|
||||
None
|
||||
@@ -140,6 +169,7 @@ impl Monitor {
|
||||
&mut self,
|
||||
target_workspace_idx: usize,
|
||||
follow: bool,
|
||||
direction: Option<OperationDirection>,
|
||||
) -> Result<()> {
|
||||
let workspace = self
|
||||
.focused_workspace_mut()
|
||||
@@ -149,22 +179,92 @@ impl Monitor {
|
||||
bail!("cannot move native maximized window to another monitor or workspace");
|
||||
}
|
||||
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
let foreground_hwnd = WindowsApi::foreground_window()?;
|
||||
let floating_window_index = workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.position(|w| w.hwnd == foreground_hwnd);
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
if let Some(idx) = floating_window_index {
|
||||
let window = workspace.floating_windows_mut().remove(idx);
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
let workspaces = self.workspaces_mut();
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
target_workspace.floating_windows_mut().push(window);
|
||||
} else {
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
match direction {
|
||||
Some(OperationDirection::Left) => match target_workspace.layout() {
|
||||
Layout::Default(layout) => match layout {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
target_workspace.add_container_to_front(container);
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace.insert_container_at_idx(0, container);
|
||||
} else {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
},
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
},
|
||||
Some(OperationDirection::Right) => match target_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(target_workspace.containers().len());
|
||||
|
||||
match layout {
|
||||
DefaultLayout::RightMainVerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace.add_container_to_back(container);
|
||||
} else {
|
||||
target_workspace
|
||||
.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_front(container);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
target_workspace.add_container(container);
|
||||
}
|
||||
|
||||
if follow {
|
||||
self.focus_workspace(target_workspace_idx)?;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user