mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-12 16:27:40 +01:00
Compare commits
21 Commits
feature/ru
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62047ed1ab | ||
|
|
7f42042322 | ||
|
|
36b9beb294 | ||
|
|
73ea3c51ef | ||
|
|
6db8f440dd | ||
|
|
0b7d5a89b7 | ||
|
|
a03a483185 | ||
|
|
6b28d76446 | ||
|
|
2325d750c5 | ||
|
|
24da24f177 | ||
|
|
dc6e326e69 | ||
|
|
8752bbbaf1 | ||
|
|
30e09d9946 | ||
|
|
8d5e40eb16 | ||
|
|
98a2aa4b23 | ||
|
|
400f90105a | ||
|
|
c6e76d2906 | ||
|
|
46e6d89770 | ||
|
|
2f0a93058f | ||
|
|
75d5971cc8 | ||
|
|
861d415551 |
17
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
17
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,16 +1,7 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
labels: [bug]
|
||||
title: "[BUG]: "
|
||||
labels: [ bug ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please **do not** open an issue for applications with invisible windows leaving ghost tiles.
|
||||
|
||||
You can run `komorebic visible-windows` when the ghost tile is present on your workspace to retrieve the invisible window's exe, class name and title, and then use that information to [ignore the window](https://lgug2z.github.io/komorebi/common-workflows/ignore-windows.html) responsible for the ghost tile.
|
||||
|
||||
If it is not possible to uniquely identify the invisible window resulting in a ghost tile through a mixture of exe, title and class identifiers, then this is not a bug with komorebi but a bug with the application you are using, and you should open an issue with the developer(s) of that application.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
@@ -19,7 +10,7 @@ body:
|
||||
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:
|
||||
@@ -29,10 +20,10 @@ body:
|
||||
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"
|
||||
```
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -2,7 +2,4 @@ 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
|
||||
about: Please search the documentation website before opening an issue
|
||||
23
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
23
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,24 +1,7 @@
|
||||
name: Feature request
|
||||
description: Suggest a new feature (Limited to Sponsors, Commercial License Holders, and Collaborators)
|
||||
labels: [enhancement]
|
||||
title: "[FEAT]: "
|
||||
description: Suggest an improvement
|
||||
labels: [ enhancement ]
|
||||
body:
|
||||
- type: dropdown
|
||||
id: Eligibility
|
||||
attributes:
|
||||
label: Eligibility
|
||||
description: >
|
||||
Feature requests are considered from individuals who are current $5+ monthly sponsors to the project, individual commercial use license holders, and approved collaborators.
|
||||
|
||||
Please specify the platform you use to sponsor the project.
|
||||
options:
|
||||
- Individual Commercial Use License
|
||||
- GitHub Sponsor
|
||||
- Ko-fi Sponsor
|
||||
- Approved Collaborator
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
@@ -26,8 +9,6 @@ body:
|
||||
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
|
||||
|
||||
7
.github/pull_request_template.md
vendored
7
.github/pull_request_template.md
vendored
@@ -1,7 +0,0 @@
|
||||
<!--
|
||||
Please follow the Conventional Commits specification.
|
||||
|
||||
If you need to update your PR with changes from `master`, please run `git rebase master`.
|
||||
|
||||
By opening this PR, you confirm that you have read and understood this project's `CONTRIBUTING.md`.
|
||||
-->
|
||||
47
.github/workflows/feature-check.yaml
vendored
47
.github/workflows/feature-check.yaml
vendored
@@ -1,47 +0,0 @@
|
||||
name: Feature Issue Check
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
|
||||
jobs:
|
||||
auto-close:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Check and close feature issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
if (issue.title.startsWith('[FEAT]: ')) {
|
||||
const message = `
|
||||
Feature requests on this repository are only open to current [GitHub sponsors](https://github.com/sponsors/LGUG2Z) on the $5/month tier and above, people with a valid [individual commercial use license](https://lgug2z.com/software/komorebi), and approved contributors.
|
||||
|
||||
This issue has been automatically closed until one of those pre-requisites can be validated.
|
||||
`.replace(/^\s+/gm, '');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: message,
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
await github.rest.issues.lock({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'resolved'
|
||||
});
|
||||
}
|
||||
269
.github/workflows/windows.yaml
vendored
269
.github/workflows/windows.yaml
vendored
@@ -15,89 +15,109 @@ on:
|
||||
- v*
|
||||
schedule:
|
||||
- cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
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
|
||||
- run: rustup toolchain install stable --profile minimal
|
||||
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: "true"
|
||||
cache-all-crates: "true"
|
||||
key: ${{ matrix.platform.target }}
|
||||
- run: cargo +nightly fmt --check
|
||||
- run: cargo clippy
|
||||
- run: cargo test --package komorebi --test compat
|
||||
- uses: houseabsolute/actions-rust-cross@v1
|
||||
with:
|
||||
command: "build"
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: "--locked --release"
|
||||
- run: |
|
||||
cargo install cargo-wix
|
||||
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
|
||||
path: |
|
||||
target/${{ matrix.platform.target }}/release/*.exe
|
||||
target/${{ matrix.platform.target }}/release/*.pdb
|
||||
target/wix/komorebi-*.msi
|
||||
retention-days: 14
|
||||
|
||||
nightly:
|
||||
needs: build
|
||||
name: Build
|
||||
runs-on: windows-latest
|
||||
permissions: write-all
|
||||
if: ${{ github.ref == 'refs/heads/master' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' ) }}
|
||||
env:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- x86_64-pc-windows-msvc
|
||||
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
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: "true"
|
||||
cache-all-crates: "true"
|
||||
- shell: bash
|
||||
- name: Prep cargo dirs
|
||||
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
|
||||
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
|
||||
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
|
||||
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: |
|
||||
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
|
||||
with:
|
||||
name: komorebi-${{ matrix.target }}
|
||||
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-bar.exe
|
||||
target/${{ matrix.target }}/release/komorebi-gui.exe
|
||||
target/${{ matrix.target }}/release/komorebi.pdb
|
||||
target/${{ matrix.target }}/release/komorebic.pdb
|
||||
target/${{ matrix.target }}/release/komorebi_gui.pdb
|
||||
target/wix/komorebi-*.msi
|
||||
retention-days: 7
|
||||
- name: Check GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
env:
|
||||
GORELEASER_CURRENT_TAG: v0.1.29
|
||||
with:
|
||||
version: latest
|
||||
args: build --skip=validate --clean
|
||||
- name: Prepare nightly artifacts
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
|
||||
run: |
|
||||
Compress-Archive .\target\${{ matrix.target }}\release\*.exe komorebi-nightly-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./target/wix/*.msi -Destination ./komorebi-nightly-x86_64.msi
|
||||
echo "$((Get-FileHash komorebi-nightly-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-nightly-x86_64-pc-windows-msvc.zip" >checksums.txt
|
||||
- name: Update nightly
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
|
||||
shell: bash
|
||||
run: |
|
||||
gh release delete nightly --yes || true
|
||||
git push origin :nightly || true
|
||||
@@ -105,102 +125,41 @@ jobs:
|
||||
--target $GITHUB_SHA \
|
||||
--prerelease \
|
||||
--title "komorebi nightly (${GITHUB_SHA})" \
|
||||
--notes-file CHANGELOG.md \
|
||||
--notes "This nightly release of komorebi corresponds to [this commit](https://github.com/LGUG2Z/komorebi/commit/${GITHUB_SHA})." \
|
||||
komorebi-nightly-x86_64-pc-windows-msvc.zip \
|
||||
komorebi-nightly-x86_64.msi \
|
||||
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
|
||||
# Release
|
||||
- name: Generate changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: |
|
||||
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
|
||||
git tag -d nightly || 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.ref_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
|
||||
git tag -d nightly
|
||||
kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md"
|
||||
- uses: softprops/action-gh-release@v2
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
body_path: "CHANGELOG.md"
|
||||
files: |
|
||||
checksums.txt
|
||||
*.zip
|
||||
*.msi
|
||||
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"
|
||||
|
||||
winget:
|
||||
name: Publish to WinGet
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@main
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: LGUG2Z.komorebi
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,5 +4,4 @@
|
||||
CHANGELOG.md
|
||||
dummy.go
|
||||
komorebic/applications.yaml
|
||||
komorebic/applications.json
|
||||
/.vs
|
||||
|
||||
68
.goreleaser.yml
Normal file
68
.goreleaser.yml
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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"
|
||||
- id: komorebi-bar
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebi-bar
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-bar.exe" ".\dist\komorebi-bar_windows_amd64_v1\komorebi-bar.exe"
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE.md
|
||||
- CHANGELOG.md
|
||||
|
||||
checksum:
|
||||
name_template: checksums.txt
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
1995
Cargo.lock
generated
1995
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
47
Cargo.toml
47
Cargo.toml
@@ -17,9 +17,9 @@ chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.31"
|
||||
egui_extras = "0.31"
|
||||
dirs = "6"
|
||||
eframe = "0.29"
|
||||
egui_extras = "0.29"
|
||||
dirs = "5"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
schemars = "0.8"
|
||||
@@ -27,38 +27,29 @@ lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
||||
serde_yaml = "0.9"
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
paste = "1"
|
||||
sysinfo = "0.33"
|
||||
sysinfo = "0.31"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "55cebdebfbd68dbd14945a1ba90f6b05b7be2893" }
|
||||
windows-numerics = { version = "0.1" }
|
||||
windows-implement = { version = "0.59" }
|
||||
windows-interface = { version = "0.59" }
|
||||
windows-core = { version = "0.60" }
|
||||
shadow-rs = "1"
|
||||
which = "7"
|
||||
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.60"
|
||||
version = "0.58"
|
||||
features = [
|
||||
"Foundation_Numerics",
|
||||
"Win32_Devices",
|
||||
"Win32_Devices_Display",
|
||||
"implement",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Power",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Accessibility",
|
||||
@@ -73,15 +64,5 @@ features = [
|
||||
"Media_Control"
|
||||
]
|
||||
|
||||
[profile.dev-jeezy]
|
||||
inherits = "dev"
|
||||
debug = false
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev-jeezy.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release-jeezy]
|
||||
inherits = "release"
|
||||
incremental = true
|
||||
codegen-units = 256
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
117
README.md
117
README.md
@@ -29,8 +29,6 @@ Tiling Window Management for Windows.
|
||||
|
||||

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

|
||||
|
||||
## Overview
|
||||
|
||||
`komorebi` is a tiling window manager that works as an extension to Microsoft's
|
||||
[Desktop Window
|
||||
Manager](https://docs.microsoft.com/en-us/windows/win32/dwm/dwm-overview) in
|
||||
@@ -17,63 +15,12 @@ system and desktop environment by default. Users are free to make such
|
||||
modifications in their own configuration files for `komorebi`, but these will
|
||||
always remain opt-in and off-by-default.
|
||||
|
||||
## Community
|
||||
|
||||
There is a [Discord server](https://discord.gg/mGkn66PHkx) available for
|
||||
`komorebi`-related discussion, help, troubleshooting etc.
|
||||
`komorebi`-related discussion, help, troubleshooting etc. If you have any
|
||||
specific feature requests or bugs to report, please create an issue on
|
||||
[GitHub](https://github.com/LGUG2Z/komorebi).
|
||||
|
||||
There is a [YouTube
|
||||
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
|
||||
`komorebi` development videos, feature previews and release overviews. Subscribing
|
||||
to the channel (which is monetized as part of the YouTube Partner Program) and
|
||||
watching videos is a really simple and passive way to contribute financially to
|
||||
the development and maintenance of `komorebi`.
|
||||
|
||||
There is an [Awesome List](https://github.com/LGUG2Z/awesome-komorebi) which
|
||||
showcases the many awesome projects that exist in the `komorebi` ecosystem.
|
||||
|
||||
## Licensing for Personal Use
|
||||
|
||||
`komorebi` is licensed under the [Komorebi 1.0.0 license](https://github.com/LGUG2Z/komorebi-license), which is a fork
|
||||
of the [PolyForm Strict 1.0.0 license](https://polyformproject.org/licenses/strict/1.0.0). On a high level this means
|
||||
that you are free to do whatever you want with `komorebi` for personal use other than redistribution, or distribution of
|
||||
new works (i.e. hard-forks) based on the software.
|
||||
|
||||
Anyone is free to make their own fork of `komorebi` with changes intended either for personal use or for integration
|
||||
back upstream via pull requests.
|
||||
|
||||
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
|
||||
i.e. using `komorebi` at work).
|
||||
|
||||
## Sponsorship for Personal Use
|
||||
|
||||
`komorebi` is a free and educational source project, and one that encourages you
|
||||
to make charitable donations if you find the software to be useful and have the
|
||||
financial means.
|
||||
|
||||
I encourage you to make a charitable donation to the [Palestine Children's
|
||||
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or to contribute
|
||||
to a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring
|
||||
me on GitHub.
|
||||
|
||||
[GitHub Sponsors is enabled for this
|
||||
project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on
|
||||
the Discord server, get shout-outs at the end of _komorebi_-related videos on
|
||||
YouTube, and gain the ability to submit feature requests on the issue tracker.
|
||||
|
||||
If you would like to tip or sponsor the project but are unable to use GitHub
|
||||
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
|
||||
make an anonymous Bitcoin donation to `bc1qv73wzspc77k46uty4vp85x8sdp24mphvm58f6q`.
|
||||
|
||||
## Licensing for Commercial Use
|
||||
|
||||
A dedicated Individual Commercial Use License is available for those who want to
|
||||
use `komorebi` at work.
|
||||
|
||||
The Individual Commerical Use License adds “Commercial Use” as a “Permitted Use”
|
||||
for the licensed individual only, for the duration of a valid paid license
|
||||
subscription only. All provisions and restrictions enumerated in the [Komorebi
|
||||
License](https://github.com/LGUG2Z/komorebi-license) continue to apply.
|
||||
|
||||
More information, pricing and purchase links for Individual Commercial Use
|
||||
Licenses [can be found here](https://lgug2z.com/software/komorebi).
|
||||
There is also a [YouTube
|
||||
channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg?sub_confirmation=1)
|
||||
where I share `komorebi` live programming videos and tutorial videos.
|
||||
|
||||
@@ -134,26 +134,6 @@ an offline machine to install.
|
||||
Once installed, proceed to get the [example configurations](example-configurations.md) (none of the commands for
|
||||
first-time set up and running komorebi require an internet connection).
|
||||
|
||||
## Upgrades
|
||||
|
||||
Before upgrading, make sure to run `komorebic stop --whkd --bar`. This is to ensure that all the current
|
||||
komorebi-related exe files can be replaced without issue.
|
||||
|
||||
Then, depending on whether you installed via `scoop` or `winget`, you can run the appropriate command:
|
||||
|
||||
```powershell
|
||||
# for winget
|
||||
winget upgrade LGUG2Z.komorebi
|
||||
```
|
||||
|
||||
```powershell
|
||||
# for scoop
|
||||
scoop update komorebi
|
||||
```
|
||||
|
||||
Once the upgrade is completed you can confirm that you have the latest version by running `komorebic --version`, and
|
||||
then start it with `komorebic start --whkd --bar`.
|
||||
|
||||
## Uninstallation
|
||||
|
||||
Before uninstalling, first run `komorebic stop --whkd --bar` to make sure that
|
||||
@@ -167,7 +147,7 @@ files created by the `quickstart` command and any other runtime files:
|
||||
|
||||
```powershell
|
||||
rm $Env:USERPROFILE\komorebi.json
|
||||
rm $Env:USERPROFILE\applications.json
|
||||
rm $Env:USERPROFILE\applications.yaml
|
||||
rm $Env:USERPROFILE\.config\whkdrc
|
||||
rm -r -Force $Env:LOCALAPPDATA\komorebi
|
||||
```
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.bar.json",
|
||||
"monitor": 0,
|
||||
"$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",
|
||||
@@ -25,11 +33,6 @@
|
||||
}
|
||||
],
|
||||
"right_widgets": [
|
||||
{
|
||||
"Update": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Media": {
|
||||
"enable": true
|
||||
@@ -70,4 +73,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.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",
|
||||
"default_workspace_padding": 20,
|
||||
|
||||
58
docs/release/v0-1-22.md
Normal file
58
docs/release/v0-1-22.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# v0.1.22
|
||||
|
||||
In addition to the [changelog](https://github.com/LGUG2Z/komorebi/releases/tag/v0.1.22) of new features and fixes,
|
||||
please note the following changes from `v0.1.21` to adjust your configuration files accordingly.
|
||||
|
||||
## tl;dr
|
||||
|
||||
The way windows are sized and drawn has been improved to remove the need to manually specify and remove invisible
|
||||
borders for applications that overflow them. If you use the active window border, the first time you launch `v0.1.22`
|
||||
you may end up with a _huge_ border due to these changes.
|
||||
|
||||
`active_window_border_width` and `active_window_border_offset` have been renamed to `border_width` and `border_offset`
|
||||
as they now also apply outside the context of the active window border.
|
||||
|
||||
```json
|
||||
{
|
||||
"active_window_border": true,
|
||||
"border_width": 8,
|
||||
"border_offset": -1
|
||||
}
|
||||
```
|
||||
|
||||
Users of the active window border should start from these settings and read the notes below before making further
|
||||
adjustments.
|
||||
|
||||
## Changes to `active_window_border`, and window sizing:
|
||||
|
||||
- The border no longer creates a second drop-shadow around the active window
|
||||
- Windows are now sized to fill the layout region entirely, ignoring window decorations such as drop shadows
|
||||
- Border offset now starts exactly at the paint edge of the window on all sides
|
||||
- Windows are sized such that the border offset and border width are taken into account
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
### Gapless
|
||||
|
||||
- Disable "transparency effects" Personalization > Colors
|
||||
- Set the following settings in `komorebi.json`:
|
||||
```json
|
||||
{
|
||||
"default_workspace_padding": 0,
|
||||
"default_container_padding": 0,
|
||||
"border_offset": -1,
|
||||
"border_width": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 1px border
|
||||
|
||||
A 1px border is drawn around the window edge. Users may see a gap for a single pixel, if the system theme has a
|
||||
transparent edge - this is the windows themed edge, and is not present for all applications.
|
||||
|
||||
```json
|
||||
{
|
||||
"border_offset": 0,
|
||||
"border_width": 1
|
||||
}
|
||||
```
|
||||
@@ -1,15 +1,5 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Phantom Tiles
|
||||
|
||||
Sometimes you may experience an application which leaves "ghost tiles" on a workspace, where there is space reserved for
|
||||
a window but no window visible.
|
||||
|
||||
You can ignore these windows by following these steps:
|
||||
|
||||
* Run `komorebic visible-windows` to find details about the invisible window
|
||||
* Using that information, [create a rule to ignore that window](common-workflows/ignore-windows.md)
|
||||
|
||||
## AutoHotKey executable not found
|
||||
|
||||
If you try to start komorebi with AHK using `komorebic start --ahk`, and you have
|
||||
@@ -95,10 +85,10 @@ running `komorebic stop` and `komorebic start`.
|
||||
|
||||
To avoid waiting an eternity:
|
||||
|
||||
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
|
||||
Settings_
|
||||
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
|
||||
Settings_
|
||||
|
||||
_Turn off the display: 1 minute_
|
||||
_Turn off the display: 1 minute_
|
||||
|
||||
Allow a minute for the display to reset, then once it actually shuts off
|
||||
allow for any additional time as prompted by your monitor for the cycle to
|
||||
|
||||
70
justfile
70
justfile
@@ -1,5 +1,4 @@
|
||||
set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"]
|
||||
|
||||
export RUST_BACKTRACE := "full"
|
||||
|
||||
clean:
|
||||
@@ -8,57 +7,36 @@ clean:
|
||||
fmt:
|
||||
cargo +nightly fmt
|
||||
cargo +stable clippy
|
||||
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 README.md
|
||||
prettier --write .goreleaser.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-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just install-target komorebic
|
||||
just install-target komorebic-no-console
|
||||
just install-target komorebi-gui
|
||||
just install-target komorebi-bar
|
||||
just install-target komorebi
|
||||
|
||||
build-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just build-target $_ }
|
||||
run:
|
||||
cargo +stable run --bin komorebi --locked
|
||||
|
||||
build-target target:
|
||||
cargo +stable build --package {{ target }} --locked --profile release-jeezy
|
||||
warn $RUST_LOG="warn":
|
||||
just run
|
||||
|
||||
build:
|
||||
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
info $RUST_LOG="info":
|
||||
just run
|
||||
|
||||
copy-target target:
|
||||
cp .\target\release-jeezy\{{ target }}.exe $Env:USERPROFILE\.cargo\bin
|
||||
debug $RUST_LOG="debug":
|
||||
just run
|
||||
|
||||
copy-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just copy-target $_ }
|
||||
|
||||
wpm target:
|
||||
just build-target {{ target }} && wpmctl stop {{ target }}; just copy-target {{ target }} && wpmctl start {{ target }}
|
||||
|
||||
copy:
|
||||
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
run target:
|
||||
cargo +stable run --bin {{ target }} --locked
|
||||
|
||||
warn target $RUST_LOG="warn":
|
||||
just run {{ target }}
|
||||
|
||||
info target $RUST_LOG="info":
|
||||
just run {{ target }}
|
||||
|
||||
debug target $RUST_LOG="debug":
|
||||
just run {{ target }}
|
||||
|
||||
trace target $RUST_LOG="trace":
|
||||
just run {{ target }}
|
||||
trace $RUST_LOG="trace":
|
||||
just run
|
||||
|
||||
deadlock $RUST_LOG="trace":
|
||||
cargo +stable run --bin komorebi --locked --features deadlock_detection
|
||||
@@ -67,15 +45,13 @@ 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 }
|
||||
|
||||
jsonschema:
|
||||
schemagen:
|
||||
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\
|
||||
|
||||
# this part is run in a nix shell because python is a nightmare
|
||||
schemagen:
|
||||
rm -rf static-config-docs bar-config-docs
|
||||
mkdir -p static-config-docs bar-config-docs
|
||||
generate-schema-doc ./schema.json --config template_name=js_offline --config minify=false ./static-config-docs/
|
||||
generate-schema-doc ./schema.bar.json --config template_name=js_offline --config minify=false ./bar-config-docs/
|
||||
mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.35"
|
||||
edition = "2024"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -16,23 +16,22 @@ crossbeam-channel = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui-phosphor = "0.9"
|
||||
egui-phosphor = "0.7"
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
netdev = "0.32"
|
||||
netdev = "0.31"
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
random_word = { version = "0.4", features = ["en"] }
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
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-core = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,20 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
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 starship_battery::units::ratio::percent;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -22,8 +22,6 @@ use std::time::Instant;
|
||||
pub struct BatteryConfig {
|
||||
/// Enable the Battery widget
|
||||
pub enable: bool,
|
||||
/// Hide the widget if the battery is at full charge
|
||||
pub hide_on_full_charge: Option<bool>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
@@ -32,19 +30,37 @@ pub struct BatteryConfig {
|
||||
|
||||
impl From<BatteryConfig> for Battery {
|
||||
fn from(value: BatteryConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
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,
|
||||
hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false),
|
||||
manager: Manager::new().unwrap(),
|
||||
last_state: String::new(),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
state: BatteryState::Discharging,
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +72,6 @@ pub enum BatteryState {
|
||||
|
||||
pub struct Battery {
|
||||
pub enable: bool,
|
||||
hide_on_full_charge: bool,
|
||||
manager: Manager,
|
||||
pub state: BatteryState,
|
||||
data_refresh_interval: u64,
|
||||
@@ -76,22 +91,17 @@ impl Battery {
|
||||
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,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if percentage == 100.0 && self.hide_on_full_charge {
|
||||
output = String::new()
|
||||
} else {
|
||||
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}%"),
|
||||
output = match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +115,7 @@ impl Battery {
|
||||
}
|
||||
|
||||
impl BarWidget for Battery {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
@@ -114,12 +124,19 @@ impl BarWidget for Battery {
|
||||
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
|
||||
};
|
||||
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -127,28 +144,17 @@ impl BarWidget for Battery {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args(["/C", "start", "ms-settings:batterysaver"])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use crate::DEFAULT_PADDING;
|
||||
use crate::render::Grouping;
|
||||
use crate::widget::WidgetConfig;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
@@ -13,87 +11,25 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.35`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.30`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar height (default: 50)
|
||||
pub height: Option<f32>,
|
||||
/// Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or
|
||||
/// vertical definition which can each take a single value for a symmetric padding or two
|
||||
/// values for each side, i.e.:
|
||||
/// ```json
|
||||
/// "padding": {
|
||||
/// "horizontal": 10
|
||||
/// }
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "padding": {
|
||||
/// "horizontal": [left, right]
|
||||
/// }
|
||||
/// ```
|
||||
/// You can also set individual padding on each side like this:
|
||||
/// ```json
|
||||
/// "padding": {
|
||||
/// "top": 10,
|
||||
/// "bottom": 10,
|
||||
/// "left": 10,
|
||||
/// "right": 10,
|
||||
/// }
|
||||
/// ```
|
||||
/// By default, padding is set to 10 on all sides.
|
||||
pub padding: Option<Padding>,
|
||||
/// Bar margin. Use one value for all sides or use a grouped margin for horizontal and/or
|
||||
/// vertical definition which can each take a single value for a symmetric margin or two
|
||||
/// values for each side, i.e.:
|
||||
/// ```json
|
||||
/// "margin": {
|
||||
/// "horizontal": 10
|
||||
/// }
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "margin": {
|
||||
/// "vertical": [top, bottom]
|
||||
/// }
|
||||
/// ```
|
||||
/// You can also set individual margin on each side like this:
|
||||
/// ```json
|
||||
/// "margin": {
|
||||
/// "top": 10,
|
||||
/// "bottom": 10,
|
||||
/// "left": 10,
|
||||
/// "right": 10,
|
||||
/// }
|
||||
/// ```
|
||||
/// By default, margin is set to 0 on all sides.
|
||||
pub margin: Option<Margin>,
|
||||
/// Bar positioning options
|
||||
#[serde(alias = "viewport")]
|
||||
pub position: Option<PositionConfig>,
|
||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
|
||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)
|
||||
pub frame: Option<FrameConfig>,
|
||||
/// The monitor index or the full monitor options
|
||||
pub monitor: MonitorConfigOrIndex,
|
||||
/// Monitor options
|
||||
pub monitor: MonitorConfig,
|
||||
/// Font family
|
||||
pub font_family: Option<String>,
|
||||
/// Font size (default: 12.5)
|
||||
pub font_size: Option<f32>,
|
||||
/// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
|
||||
pub icon_scale: Option<f32>,
|
||||
/// Max label width before text truncation (default: 400.0)
|
||||
pub max_label_width: Option<f32>,
|
||||
/// Theme
|
||||
pub theme: Option<KomobarTheme>,
|
||||
/// Alpha value for the color transparency [[0-255]] (default: 200)
|
||||
pub transparency_alpha: Option<u8>,
|
||||
/// Spacing between widgets (default: 10.0)
|
||||
pub widget_spacing: Option<f32>,
|
||||
/// Visual grouping for widgets
|
||||
pub grouping: Option<Grouping>,
|
||||
/// Left side widgets (ordered left-to-right)
|
||||
pub left_widgets: Vec<WidgetConfig>,
|
||||
/// Center widgets (ordered left-to-right)
|
||||
pub center_widgets: Option<Vec<WidgetConfig>>,
|
||||
/// Right side widgets (ordered left-to-right)
|
||||
pub right_widgets: Vec<WidgetConfig>,
|
||||
}
|
||||
@@ -115,9 +51,7 @@ impl KomobarConfig {
|
||||
}
|
||||
|
||||
if display {
|
||||
println!(
|
||||
"\nYour bar configuration file contains some options that have been renamed or deprecated:\n"
|
||||
);
|
||||
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) {
|
||||
@@ -127,15 +61,6 @@ impl KomobarConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_all_icons_on_komorebi_workspace(widgets: &[WidgetConfig]) -> bool {
|
||||
widgets
|
||||
.iter()
|
||||
.any(|w| matches!(w, WidgetConfig::Komorebi(config) if config.workspaces.is_some_and(|w| w.enable && w.display.is_some_and(|s| matches!(s,
|
||||
WorkspacesDisplayFormat::AllIcons
|
||||
| WorkspacesDisplayFormat::AllIconsAndText
|
||||
| WorkspacesDisplayFormat::AllIconsAndTextOnSelected)))))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -154,15 +79,6 @@ pub struct FrameConfig {
|
||||
pub inner_margin: Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum MonitorConfigOrIndex {
|
||||
/// The monitor index where you want the bar to show
|
||||
Index(usize),
|
||||
/// The full monitor options with the index and an optional work_area_offset
|
||||
MonitorConfig(MonitorConfig),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MonitorConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
@@ -171,154 +87,6 @@ pub struct MonitorConfig {
|
||||
pub work_area_offset: Option<Rect>,
|
||||
}
|
||||
|
||||
pub type Padding = SpacingKind;
|
||||
pub type Margin = SpacingKind;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
// WARNING: To any developer messing with this code in the future: The order here matters!
|
||||
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
|
||||
// `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the
|
||||
// individual values.
|
||||
pub enum SpacingKind {
|
||||
All(f32),
|
||||
Individual(IndividualSpacingConfig),
|
||||
Grouped(GroupedSpacingConfig),
|
||||
}
|
||||
|
||||
impl SpacingKind {
|
||||
pub fn to_individual(&self, default: f32) -> IndividualSpacingConfig {
|
||||
match self {
|
||||
SpacingKind::All(m) => IndividualSpacingConfig::all(*m),
|
||||
SpacingKind::Grouped(grouped_spacing_config) => {
|
||||
let vm = grouped_spacing_config.vertical.as_ref().map_or(
|
||||
IndividualSpacingConfig::vertical(default),
|
||||
|vm| match vm {
|
||||
GroupedSpacingOptions::Symmetrical(m) => {
|
||||
IndividualSpacingConfig::vertical(*m)
|
||||
}
|
||||
GroupedSpacingOptions::Split(tm, bm) => {
|
||||
IndividualSpacingConfig::vertical(*tm).bottom(*bm)
|
||||
}
|
||||
},
|
||||
);
|
||||
let hm = grouped_spacing_config.horizontal.as_ref().map_or(
|
||||
IndividualSpacingConfig::horizontal(default),
|
||||
|hm| match hm {
|
||||
GroupedSpacingOptions::Symmetrical(m) => {
|
||||
IndividualSpacingConfig::horizontal(*m)
|
||||
}
|
||||
GroupedSpacingOptions::Split(lm, rm) => {
|
||||
IndividualSpacingConfig::horizontal(*lm).right(*rm)
|
||||
}
|
||||
},
|
||||
);
|
||||
IndividualSpacingConfig {
|
||||
top: vm.top,
|
||||
bottom: vm.bottom,
|
||||
left: hm.left,
|
||||
right: hm.right,
|
||||
}
|
||||
}
|
||||
SpacingKind::Individual(m) => *m,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GroupedSpacingConfig {
|
||||
pub vertical: Option<GroupedSpacingOptions>,
|
||||
pub horizontal: Option<GroupedSpacingOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum GroupedSpacingOptions {
|
||||
Symmetrical(f32),
|
||||
Split(f32, f32),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct IndividualSpacingConfig {
|
||||
pub top: f32,
|
||||
pub bottom: f32,
|
||||
pub left: f32,
|
||||
pub right: f32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl IndividualSpacingConfig {
|
||||
pub const ZERO: Self = IndividualSpacingConfig {
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
};
|
||||
|
||||
pub fn all(value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
top: value,
|
||||
bottom: value,
|
||||
left: value,
|
||||
right: value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn horizontal(value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
left: value,
|
||||
right: value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
top: value,
|
||||
bottom: value,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn top(self, value: f32) -> Self {
|
||||
IndividualSpacingConfig { top: value, ..self }
|
||||
}
|
||||
|
||||
pub fn bottom(self, value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
bottom: value,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left(self, value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
left: value,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn right(self, value: f32) -> Self {
|
||||
IndividualSpacingConfig {
|
||||
right: value,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_individual_spacing(
|
||||
default: f32,
|
||||
spacing: &Option<SpacingKind>,
|
||||
) -> IndividualSpacingConfig {
|
||||
spacing
|
||||
.as_ref()
|
||||
.map_or(IndividualSpacingConfig::all(default), |s| {
|
||||
s.to_individual(default)
|
||||
})
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
@@ -329,10 +97,7 @@ impl KomobarConfig {
|
||||
|
||||
if value.frame.is_none() {
|
||||
value.frame = Some(FrameConfig {
|
||||
inner_margin: Position {
|
||||
x: DEFAULT_PADDING,
|
||||
y: DEFAULT_PADDING,
|
||||
},
|
||||
inner_margin: Position { x: 10.0, y: 10.0 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -371,13 +136,11 @@ impl From<Position> for Pos2 {
|
||||
pub enum KomobarTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
name: komorebi_themes::Base16,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
@@ -413,104 +176,3 @@ pub enum LabelPrefix {
|
||||
/// Show an icon and text
|
||||
IconAndText,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum DisplayFormat {
|
||||
/// Show only icon
|
||||
Icon,
|
||||
/// Show only text
|
||||
Text,
|
||||
/// Show an icon and text for the selected element, and text on the rest
|
||||
TextAndIconOnSelected,
|
||||
/// Show both icon and text
|
||||
IconAndText,
|
||||
/// Show an icon and text for the selected element, and icons on the rest
|
||||
IconAndTextOnSelected,
|
||||
}
|
||||
|
||||
macro_rules! extend_enum {
|
||||
($existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
|
||||
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema, PartialEq)]
|
||||
pub enum $new_enum {
|
||||
// Add new variants
|
||||
$(
|
||||
$(#[$meta])*
|
||||
$variant,
|
||||
)*
|
||||
// Include a variant that wraps the existing enum and flatten it when deserializing
|
||||
#[serde(untagged)]
|
||||
Existing($existing_enum),
|
||||
}
|
||||
|
||||
// Implement From for the existing enum
|
||||
impl From<$existing_enum> for $new_enum {
|
||||
fn from(value: $existing_enum) -> Self {
|
||||
$new_enum::Existing(value)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
|
||||
/// Show all icons only
|
||||
AllIcons,
|
||||
/// Show both all icons and text
|
||||
AllIconsAndText,
|
||||
/// Show all icons and text for the selected element, and all icons on the rest
|
||||
AllIconsAndTextOnSelected,
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum OriginalDisplayFormat {
|
||||
/// Show None Of The Things
|
||||
NoneOfTheThings,
|
||||
}
|
||||
|
||||
extend_enum!(OriginalDisplayFormat, ExtendedDisplayFormat, {
|
||||
/// Show Some Of The Things
|
||||
SomeOfTheThings,
|
||||
});
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ExampleConfig {
|
||||
#[allow(unused)]
|
||||
format: ExtendedDisplayFormat,
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn extend_new_variant() {
|
||||
let raw = json!({
|
||||
"format": "SomeOfTheThings",
|
||||
})
|
||||
.to_string();
|
||||
|
||||
assert!(serde_json::from_str::<ExampleConfig>(&raw).is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn extend_existing_variant() {
|
||||
let raw = json!({
|
||||
"format": "NoneOfTheThings",
|
||||
})
|
||||
.to_string();
|
||||
|
||||
assert!(serde_json::from_str::<ExampleConfig>(&raw).is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn extend_invalid_variant() {
|
||||
let raw = json!({
|
||||
"format": "ALLOFTHETHINGS",
|
||||
})
|
||||
.to_string();
|
||||
|
||||
assert!(serde_json::from_str::<ExampleConfig>(&raw).is_err())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
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;
|
||||
@@ -29,18 +30,17 @@ pub struct CpuConfig {
|
||||
|
||||
impl From<CpuConfig> for Cpu {
|
||||
fn from(value: CpuConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
|
||||
|
||||
system.refresh_cpu_usage();
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system: System::new_with_specifics(
|
||||
RefreshKind::default().without_memory().without_processes(),
|
||||
),
|
||||
data_refresh_interval,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,10 +70,17 @@ impl Cpu {
|
||||
}
|
||||
|
||||
impl BarWidget for Cpu {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 => {
|
||||
@@ -81,7 +88,7 @@ impl BarWidget for Cpu {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -89,27 +96,25 @@ impl BarWidget for Cpu {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
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()
|
||||
{
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,19 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::Align;
|
||||
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 eframe::egui::text::LayoutJob;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Custom format with additive modifiers for integer format specifiers
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CustomModifiers {
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
format: String,
|
||||
/// Additive modifiers for integer format specifiers (e.g. { "%U": 1 } to increment the zero-indexed week number by 1)
|
||||
modifiers: std::collections::HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl CustomModifiers {
|
||||
fn apply(&self, output: &str) -> String {
|
||||
let int_formatters = vec![
|
||||
"%Y", "%C", "%y", "%m", "%d", "%e", "%w", "%u", "%U", "%W", "%G", "%g", "%V", "%j",
|
||||
"%H", "%k", "%I", "%l", "%M", "%S", "%f",
|
||||
];
|
||||
|
||||
let mut modified_output = output.to_string();
|
||||
|
||||
for (modifier, value) in &self.modifiers {
|
||||
// check if formatter is integer type
|
||||
if !int_formatters.contains(&modifier.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// get the strftime value of modifier
|
||||
let formatted_modifier = chrono::Local::now().format(modifier).to_string();
|
||||
|
||||
// find the gotten value in the original output
|
||||
if let Some(pos) = modified_output.find(&formatted_modifier) {
|
||||
let start = pos;
|
||||
let end = start + formatted_modifier.len();
|
||||
// replace that value with the modified value
|
||||
if let Ok(num) = formatted_modifier.parse::<i32>() {
|
||||
modified_output.replace_range(start..end, &(num + value).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modified_output
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DateConfig {
|
||||
/// Enable the Date widget
|
||||
@@ -87,8 +46,6 @@ pub enum DateFormat {
|
||||
DayDateMonthYear,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
/// Custom format with modifiers
|
||||
CustomModifiers(CustomModifiers),
|
||||
}
|
||||
|
||||
impl DateFormat {
|
||||
@@ -102,14 +59,13 @@ impl DateFormat {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fmt_string(&self) -> String {
|
||||
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(),
|
||||
DateFormat::CustomModifiers(custom) => custom.format.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,23 +79,24 @@ pub struct Date {
|
||||
|
||||
impl Date {
|
||||
fn output(&mut self) -> String {
|
||||
let formatted = chrono::Local::now()
|
||||
chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string();
|
||||
|
||||
// if custom modifiers are used, apply them
|
||||
match &self.format {
|
||||
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
|
||||
_ => formatted,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Date {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 => {
|
||||
@@ -147,7 +104,7 @@ impl BarWidget for Date {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -159,28 +116,22 @@ impl BarWidget for Date {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false),
|
||||
)
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.format.next()
|
||||
}
|
||||
});
|
||||
if ui
|
||||
.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.next()
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows::Win32::Globalization::LCIDToLocaleName;
|
||||
use windows::Win32::Globalization::LOCALE_ALLOW_NEUTRAL_NAMES;
|
||||
use windows::Win32::System::SystemServices::LOCALE_NAME_MAX_LENGTH;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
|
||||
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
|
||||
const ERROR_TEXT: &str = "Error";
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KeyboardConfig {
|
||||
/// Enable the Input widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 1 second)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<KeyboardConfig> for Keyboard {
|
||||
fn from(value: KeyboardConfig) -> Self {
|
||||
let data_refresh_interval = value
|
||||
.data_refresh_interval
|
||||
.unwrap_or(DEFAULT_DATA_REFRESH_INTERVAL);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
lang_name: get_lang(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Keyboard {
|
||||
pub enable: bool,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
lang_name: String,
|
||||
}
|
||||
|
||||
/// Retrieves the name of the active keyboard layout for the current foreground window.
|
||||
///
|
||||
/// This function determines the active keyboard layout by querying the system for the
|
||||
/// foreground window's thread ID and its associated keyboard layout. It then attempts
|
||||
/// to retrieve the locale name corresponding to the keyboard layout.
|
||||
///
|
||||
/// # Failure Cases
|
||||
///
|
||||
/// This function can fail in two distinct scenarios:
|
||||
///
|
||||
/// 1. **Failure to Retrieve the Locale Name**:
|
||||
/// If the system fails to retrieve the locale name (e.g., due to an invalid or unsupported
|
||||
/// language identifier), the function will return `Err(())`.
|
||||
///
|
||||
/// 2. **Invalid UTF-16 Characters in the Locale Name**:
|
||||
/// If the retrieved locale name contains invalid UTF-16 sequences, the conversion to a Rust
|
||||
/// `String` will fail, and the function will return `Err(())`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
|
||||
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
|
||||
/// invalid UTF-16 characters during conversion.
|
||||
fn get_active_keyboard_layout() -> Result<String, ()> {
|
||||
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
|
||||
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
|
||||
|
||||
// Extract the low word (language identifier) from the keyboard layout handle.
|
||||
let lang_id = (lcid.0 as u32) & 0xFFFF;
|
||||
let mut locale_name_buffer = [0; LOCALE_NAME_MAX_LENGTH as usize];
|
||||
let char_count = unsafe {
|
||||
LCIDToLocaleName(
|
||||
lang_id,
|
||||
Some(&mut locale_name_buffer),
|
||||
LOCALE_ALLOW_NEUTRAL_NAMES,
|
||||
)
|
||||
};
|
||||
|
||||
match char_count {
|
||||
0 => Err(()),
|
||||
_ => String::from_utf16(&locale_name_buffer[..char_count as usize]).map_err(|_| ()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the name of the active keyboard layout or a fallback error message.
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// - **Success Case**:
|
||||
/// If [`get_active_keyboard_layout`] succeeds, this function returns the retrieved keyboard
|
||||
/// layout name as a `String`.
|
||||
///
|
||||
/// - **Failure Case**:
|
||||
/// If [`get_active_keyboard_layout`] fails, this function returns the value of `ERROR_TEXT`
|
||||
/// as a fallback message. This ensures that the function always returns a valid `String`,
|
||||
/// even in error scenarios.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `String` representing either:
|
||||
/// - The name of the active keyboard layout, or
|
||||
/// - The fallback error message (`ERROR_TEXT`) if the layout name cannot be retrieved.
|
||||
fn get_lang() -> String {
|
||||
get_active_keyboard_layout()
|
||||
.map(|l| l.trim_end_matches('\0').to_string())
|
||||
.unwrap_or_else(|_| ERROR_TEXT.to_string())
|
||||
}
|
||||
|
||||
impl Keyboard {
|
||||
fn output(&mut self) -> String {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.last_updated = now;
|
||||
self.lang_name = get_lang();
|
||||
}
|
||||
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("KB: {}", self.lang_name),
|
||||
LabelPrefix::None | LabelPrefix::Icon => self.lang_name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Keyboard {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::KEYBOARD.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,313 +0,0 @@
|
||||
use crate::config::DisplayFormat;
|
||||
use crate::komorebi::KomorebiLayoutConfig;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::vec2;
|
||||
use komorebi_client::SocketMessage;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::de::Error;
|
||||
use serde_json::from_str;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum KomorebiLayout {
|
||||
Default(komorebi_client::DefaultLayout),
|
||||
Monocle,
|
||||
Floating,
|
||||
Paused,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: String = String::deserialize(deserializer)?;
|
||||
|
||||
// Attempt to deserialize the string as a DefaultLayout
|
||||
if let Ok(default_layout) =
|
||||
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
|
||||
{
|
||||
return Ok(KomorebiLayout::Default(default_layout));
|
||||
}
|
||||
|
||||
// Handle other cases
|
||||
match s.as_str() {
|
||||
"Monocle" => Ok(KomorebiLayout::Monocle),
|
||||
"Floating" => Ok(KomorebiLayout::Floating),
|
||||
"Paused" => Ok(KomorebiLayout::Paused),
|
||||
"Custom" => Ok(KomorebiLayout::Custom),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for KomorebiLayout {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
|
||||
KomorebiLayout::Monocle => write!(f, "Monocle"),
|
||||
KomorebiLayout::Floating => write!(f, "Floating"),
|
||||
KomorebiLayout::Paused => write!(f, "Paused"),
|
||||
KomorebiLayout::Custom => write!(f, "Custom"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KomorebiLayout {
|
||||
fn is_default(&mut self) -> bool {
|
||||
matches!(self, KomorebiLayout::Default(_))
|
||||
}
|
||||
|
||||
fn on_click(
|
||||
&mut self,
|
||||
show_options: &bool,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: Option<usize>,
|
||||
) -> bool {
|
||||
if self.is_default() {
|
||||
!show_options
|
||||
} else {
|
||||
self.on_click_option(monitor_idx, workspace_idx);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
|
||||
match self {
|
||||
KomorebiLayout::Default(option) => {
|
||||
if let Some(ws_idx) = workspace_idx {
|
||||
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
||||
monitor_idx,
|
||||
ws_idx,
|
||||
*option,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
||||
}
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Monocle => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() {
|
||||
tracing::error!("could not send message to komorebi: ToggleMonocle");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Floating => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() {
|
||||
tracing::error!("could not send message to komorebi: ToggleTiling");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Paused => {
|
||||
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
|
||||
tracing::error!("could not send message to komorebi: TogglePause");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Custom => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_icon(&mut self, is_selected: bool, font_id: FontId, ctx: &Context, ui: &mut Ui) {
|
||||
// paint custom icons for the layout
|
||||
let size = Vec2::splat(font_id.size);
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::hover());
|
||||
let color = if is_selected {
|
||||
ctx.style().visuals.selection.stroke.color
|
||||
} else {
|
||||
ui.style().visuals.text_color()
|
||||
};
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
let mut rect = response.rect;
|
||||
let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
|
||||
|
||||
match self {
|
||||
KomorebiLayout::Default(layout) => match layout {
|
||||
komorebi_client::DefaultLayout::BSP => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::Columns => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::Rows => {
|
||||
painter.line_segment([c - vec2(r, r / 2.0), c + vec2(r, -r / 2.0)], stroke);
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(r, -r / 2.0), c + vec2(r, r / 2.0)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::VerticalStack => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(r, 0.0), c], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::HorizontalStack => {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c, c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::Grid => {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
},
|
||||
KomorebiLayout::Monocle => {}
|
||||
KomorebiLayout::Floating => {
|
||||
let mut rect_left = response.rect;
|
||||
rect_left.set_width(rect.width() * 0.5);
|
||||
rect_left.set_height(rect.height() * 0.5);
|
||||
let mut rect_right = rect_left;
|
||||
rect_left = rect_left.translate(Vec2::new(
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
));
|
||||
rect_right = rect_right.translate(Vec2::new(
|
||||
rect.width() * 0.35 + stroke.width,
|
||||
rect.width() * 0.35 + stroke.width,
|
||||
));
|
||||
painter.rect_filled(rect_left, rounding, color);
|
||||
painter.rect_stroke(rect_right, rounding, stroke, StrokeKind::Outside);
|
||||
}
|
||||
KomorebiLayout::Paused => {
|
||||
let mut rect_left = response.rect;
|
||||
rect_left.set_width(rect.width() * 0.25);
|
||||
rect_left.set_height(rect.height() * 0.8);
|
||||
let mut rect_right = rect_left;
|
||||
rect_left = rect_left.translate(Vec2::new(
|
||||
rect.width() * 0.2 + stroke.width,
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
));
|
||||
rect_right = rect_right.translate(Vec2::new(
|
||||
rect.width() * 0.55 + stroke.width,
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
));
|
||||
painter.rect_filled(rect_left, rounding, color);
|
||||
painter.rect_filled(rect_right, rounding, color);
|
||||
}
|
||||
KomorebiLayout::Custom => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c + vec2(0.0, r / 2.0), c + vec2(r, r / 2.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r / 3.0), c - vec2(r, r / 3.0)], stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
ui: &mut Ui,
|
||||
render_config: &mut RenderConfig,
|
||||
layout_config: &KomorebiLayoutConfig,
|
||||
workspace_idx: Option<usize>,
|
||||
) {
|
||||
let monitor_idx = render_config.monitor_idx;
|
||||
let font_id = render_config.icon_font_id.clone();
|
||||
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
|
||||
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);
|
||||
|
||||
if !self.is_default() {
|
||||
show_options = false;
|
||||
}
|
||||
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
let layout_frame = SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
|
||||
self.show_icon(false, font_id.clone(), ctx, ui);
|
||||
}
|
||||
|
||||
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
|
||||
ui.add(Label::new(self.to_string()).selectable(false));
|
||||
}
|
||||
})
|
||||
.on_hover_text(self.to_string());
|
||||
|
||||
if layout_frame.clicked() {
|
||||
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
|
||||
}
|
||||
|
||||
if show_options {
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
Frame::NONE.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||
.selectable(false),
|
||||
);
|
||||
|
||||
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::HorizontalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
||||
//KomorebiLayout::Custom,
|
||||
KomorebiLayout::Monocle,
|
||||
KomorebiLayout::Floating,
|
||||
KomorebiLayout::Paused,
|
||||
]);
|
||||
|
||||
for layout_option in &mut layout_options {
|
||||
let is_selected = self == layout_option;
|
||||
|
||||
if SelectableFrame::new(is_selected)
|
||||
.show(ui, |ui| {
|
||||
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
|
||||
})
|
||||
.on_hover_text(match layout_option {
|
||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
||||
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
||||
KomorebiLayout::Custom => "Custom".to_string(),
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
||||
show_options = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
RenderConfig::store_show_komorebi_layout_options(show_options);
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,13 @@ mod battery;
|
||||
mod config;
|
||||
mod cpu;
|
||||
mod date;
|
||||
mod keyboard;
|
||||
mod komorebi;
|
||||
mod komorebi_layout;
|
||||
mod media;
|
||||
mod memory;
|
||||
mod network;
|
||||
mod render;
|
||||
mod selected_frame;
|
||||
mod storage;
|
||||
mod time;
|
||||
mod ui;
|
||||
mod update;
|
||||
mod widget;
|
||||
|
||||
use crate::bar::Komobar;
|
||||
@@ -22,46 +17,38 @@ use crate::config::KomobarConfig;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use clap::Parser;
|
||||
use config::MonitorConfigOrIndex;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::SubscribeOptions;
|
||||
use schemars::r#gen::SchemaSettings;
|
||||
use std::collections::HashMap;
|
||||
use schemars::gen::SchemaSettings;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
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::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||
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;
|
||||
use windows_core::BOOL;
|
||||
|
||||
pub static WIDGET_SPACING: f32 = 10.0;
|
||||
|
||||
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
|
||||
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
|
||||
pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
|
||||
pub static BAR_HEIGHT: f32 = 50.0;
|
||||
pub static DEFAULT_PADDING: f32 = 10.0;
|
||||
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
@@ -115,11 +102,6 @@ fn process_hwnd() -> Option<isize> {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum KomorebiEvent {
|
||||
Notification(komorebi_client::Notification),
|
||||
Reconnect,
|
||||
}
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }?;
|
||||
|
||||
@@ -132,8 +114,8 @@ fn main() -> color_eyre::Result<()> {
|
||||
s.inline_subschemas = true;
|
||||
});
|
||||
|
||||
let r#gen = settings.into_generator();
|
||||
let socket_message = r#gen.into_root_schema_for::<KomobarConfig>();
|
||||
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}");
|
||||
@@ -149,15 +131,13 @@ fn main() -> color_eyre::Result<()> {
|
||||
}
|
||||
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
// TODO: Audit that the environment access only happens in single-threaded code.
|
||||
unsafe { std::env::set_var("RUST_LIB_BACKTRACE", "1") };
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
|
||||
color_eyre::install()?;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
// TODO: Audit that the environment access only happens in single-threaded code.
|
||||
unsafe { std::env::set_var("RUST_LOG", "info") };
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
tracing::subscriber::set_global_default(
|
||||
@@ -239,43 +219,30 @@ fn main() -> color_eyre::Result<()> {
|
||||
&SocketMessage::State,
|
||||
)?)?;
|
||||
|
||||
let (usr_monitor_index, work_area_offset) = match &config.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
|
||||
(monitor_config.index, monitor_config.work_area_offset)
|
||||
}
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
};
|
||||
let monitor_index = state
|
||||
.monitor_usr_idx_map
|
||||
.get(&usr_monitor_index)
|
||||
.map_or(usr_monitor_index, |i| *i);
|
||||
|
||||
MONITOR_RIGHT.store(
|
||||
state.monitors.elements()[monitor_index].size().right,
|
||||
state.monitors.elements()[config.monitor.index].size().right,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[monitor_index].size().top,
|
||||
state.monitors.elements()[config.monitor.index].size().top,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_LEFT.store(
|
||||
state.monitors.elements()[monitor_index].size().left,
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[config.monitor.index].size().left,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_INDEX.store(monitor_index, Ordering::SeqCst);
|
||||
|
||||
match config.position {
|
||||
None => {
|
||||
config.position = Some(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
||||
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()[monitor_index].size().right as f32,
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32,
|
||||
y: 50.0,
|
||||
}),
|
||||
})
|
||||
@@ -283,14 +250,14 @@ fn main() -> color_eyre::Result<()> {
|
||||
Some(ref mut position) => {
|
||||
if position.start.is_none() {
|
||||
position.start = Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
||||
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()[monitor_index].size().right as f32,
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32,
|
||||
y: 50.0,
|
||||
})
|
||||
}
|
||||
@@ -299,7 +266,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let viewport_builder = ViewportBuilder::default()
|
||||
.with_decorations(false)
|
||||
.with_transparent(true)
|
||||
// .with_transparent(config.transparent)
|
||||
.with_taskbar(false);
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
@@ -307,9 +274,15 @@ fn main() -> color_eyre::Result<()> {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(rect) = &work_area_offset {
|
||||
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(monitor_index, *rect))?;
|
||||
tracing::info!("work area offset applied to monitor: {}", monitor_index);
|
||||
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();
|
||||
@@ -339,10 +312,13 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
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));
|
||||
@@ -351,7 +327,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let ctx_komorebi = cc.egui_ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let subscriber_name = format!("komorebi-bar-{}", random_word::r#gen(random_word::Lang::En));
|
||||
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,
|
||||
@@ -363,10 +339,6 @@ fn main() -> color_eyre::Result<()> {
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(subscription) => {
|
||||
match subscription.set_read_timeout(Some(Duration::from_secs(1))) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(subscription);
|
||||
|
||||
@@ -385,12 +357,18 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
tracing::info!("reconnected to komorebi");
|
||||
|
||||
if let Err(error) = tx_gui.send(KomorebiEvent::Reconnect) {
|
||||
tracing::error!("could not send komorebi reconnect event to gui thread: {error}")
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
ctx_komorebi.request_repaint();
|
||||
continue;
|
||||
}
|
||||
|
||||
match String::from_utf8(buffer) {
|
||||
@@ -401,7 +379,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
Ok(notification) => {
|
||||
tracing::debug!("received notification from komorebi");
|
||||
|
||||
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
|
||||
if let Err(error) = tx_gui.send(notification) {
|
||||
tracing::error!("could not send komorebi notification update to gui thread: {error}")
|
||||
}
|
||||
|
||||
@@ -426,7 +404,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
|
||||
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config_arc)))
|
||||
}),
|
||||
)
|
||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::Align;
|
||||
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 eframe::egui::text::LayoutJob;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -77,13 +78,20 @@ impl Media {
|
||||
}
|
||||
|
||||
impl BarWidget for Media {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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(),
|
||||
config.icon_font_id.clone(),
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -91,33 +99,29 @@ impl BarWidget for Media {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
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(),
|
||||
)
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.toggle();
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
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;
|
||||
@@ -29,18 +30,17 @@ pub struct MemoryConfig {
|
||||
|
||||
impl From<MemoryConfig> for Memory {
|
||||
fn from(value: MemoryConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
|
||||
|
||||
system.refresh_memory();
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system: System::new_with_specifics(
|
||||
RefreshKind::default().without_cpu().without_processes(),
|
||||
),
|
||||
data_refresh_interval,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,10 +73,17 @@ impl Memory {
|
||||
}
|
||||
|
||||
impl BarWidget for Memory {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 => {
|
||||
@@ -84,7 +91,7 @@ impl BarWidget for Memory {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -92,27 +99,25 @@ impl BarWidget for Memory {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
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()
|
||||
{
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
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;
|
||||
@@ -26,8 +27,6 @@ pub struct NetworkConfig {
|
||||
pub show_total_data_transmitted: bool,
|
||||
/// Show network activity
|
||||
pub show_network_activity: bool,
|
||||
/// Show default interface
|
||||
pub show_default_interface: Option<bool>,
|
||||
/// Characters to reserve for network activity data
|
||||
pub network_activity_fill_characters: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
@@ -38,40 +37,130 @@ pub struct NetworkConfig {
|
||||
|
||||
impl From<NetworkConfig> for Network {
|
||||
fn from(value: NetworkConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
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,
|
||||
show_total_activity: value.show_total_data_transmitted,
|
||||
show_activity: value.show_network_activity,
|
||||
show_default_interface: value.show_default_interface.unwrap_or(true),
|
||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
||||
default_interface: String::new(),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
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_activity: vec![],
|
||||
last_state_activity: vec![],
|
||||
last_updated_network_activity: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
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_activity: bool,
|
||||
pub show_activity: bool,
|
||||
pub show_default_interface: 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_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
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,
|
||||
}
|
||||
@@ -85,273 +174,214 @@ impl Network {
|
||||
}
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let mut activity = self.last_state_activity.clone();
|
||||
let mut total_activity = self.last_state_total_activity.clone();
|
||||
fn network_activity(&mut self) -> Vec<String> {
|
||||
let mut outputs = self.last_state_network_activity.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
if self.show_network_activity
|
||||
&& now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
activity.clear();
|
||||
total_activity.clear();
|
||||
outputs.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
|
||||
self.networks_network_activity.refresh(true);
|
||||
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if self.show_activity {
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
Self::to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval,
|
||||
if self.show_network_activity {
|
||||
self.networks_network_activity.refresh();
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
outputs.push(match self.label_prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{: >width$}/s | {: >width$}/s",
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
Self::to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval,
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {: >width$}/s | {} {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if self.show_total_activity {
|
||||
total_activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Total,
|
||||
Self::to_pretty_bytes(data.total_received(), 1),
|
||||
Self::to_pretty_bytes(data.total_transmitted(), 1),
|
||||
))
|
||||
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_activity.clone_from(&activity);
|
||||
self.last_state_total_activity.clone_from(&total_activity);
|
||||
self.last_state_network_activity.clone_from(&outputs);
|
||||
self.last_updated_network_activity = now;
|
||||
}
|
||||
|
||||
(activity, total_activity)
|
||||
outputs
|
||||
}
|
||||
|
||||
fn reading_to_label(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
reading: NetworkReading,
|
||||
config: RenderConfig,
|
||||
) -> Label {
|
||||
let (text_down, text_up) = match self.label_prefix {
|
||||
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"{: >width$}/s ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"{: >width$}/s",
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("{} ", reading.received_text),
|
||||
reading.transmitted_text,
|
||||
),
|
||||
},
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"DOWN: {: >width$}/s ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"UP: {: >width$}/s",
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("\u{2211}DOWN: {}/s ", reading.received_text),
|
||||
format!("\u{2211}UP: {}/s", reading.transmitted_text),
|
||||
),
|
||||
},
|
||||
};
|
||||
fn total_data_transmitted(&mut self) -> Vec<String> {
|
||||
let mut outputs = self.last_state_total_data_transmitted.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
let icon_format = TextFormat::simple(
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
);
|
||||
let text_format = TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
};
|
||||
if self.show_total_data_transmitted
|
||||
&& now.duration_since(self.last_updated_total_data_transmitted)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
outputs.clear();
|
||||
|
||||
// icon
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
|
||||
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),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
icon_format.font_id.clone(),
|
||||
icon_format.color,
|
||||
100.0,
|
||||
);
|
||||
}
|
||||
|
||||
// text
|
||||
layout_job.append(
|
||||
&text_down,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
// icon
|
||||
layout_job.append(
|
||||
&match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ARROW_FAT_UP.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
0.0,
|
||||
icon_format.clone(),
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job.append(
|
||||
&text_up,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
Label::new(layout_job).selectable(false)
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
|
||||
let input = input_in_bytes as f32 / timespan_in_s as f32;
|
||||
let mut magnitude = input.log(1024f32) as u32;
|
||||
|
||||
// let the base unit be KiB
|
||||
if magnitude < 1 {
|
||||
magnitude = 1;
|
||||
self.last_state_total_data_transmitted.clone_from(&outputs);
|
||||
self.last_updated_total_data_transmitted = now;
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
outputs
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Network {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
// widget spacing: make sure to use the same config to call the apply_on_widget function
|
||||
let mut render_config = config.clone();
|
||||
|
||||
if self.show_total_activity || self.show_activity {
|
||||
let (activity, total_activity) = self.network_activity();
|
||||
|
||||
if self.show_total_activity {
|
||||
for reading in total_activity {
|
||||
render_config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading, config.clone()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_activity {
|
||||
for reading in activity {
|
||||
render_config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading, config.clone()));
|
||||
});
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
if self.show_default_interface {
|
||||
self.default_interface();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::WIFI_HIGH.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
self.default_interface.insert_str(0, "NET: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&self.default_interface,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// widget spacing: pass on the config that was use for calling the apply_on_widget function
|
||||
*config = render_config.clone();
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum NetworkReadingFormat {
|
||||
Speed = 0,
|
||||
Total = 1,
|
||||
}
|
||||
if self.show_network_activity {
|
||||
for output in self.network_activity() {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NetworkReading {
|
||||
pub format: NetworkReadingFormat,
|
||||
pub received_text: String,
|
||||
pub transmitted_text: String,
|
||||
}
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
impl NetworkReading {
|
||||
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
|
||||
NetworkReading {
|
||||
format,
|
||||
received_text: received,
|
||||
transmitted_text: transmitted,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,3 +404,22 @@ impl fmt::Display for DataUnit {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
|
||||
let input = input_in_bytes as f32 / timespan_in_s as f32;
|
||||
let mut magnitude = input.log(1024f32) as u32;
|
||||
|
||||
// let the base unit be KiB
|
||||
if magnitude < 1 {
|
||||
magnitude = 1;
|
||||
}
|
||||
|
||||
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
|
||||
let result = input / ((1u64) << (magnitude * 10)) as f32;
|
||||
|
||||
match base {
|
||||
Some(DataUnit::B) => format!("{result:.1} B"),
|
||||
Some(unit) => format!("{result:.1} {unit}iB"),
|
||||
None => String::from("Unknown data unit"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,411 +0,0 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::InnerResponse;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Shadow;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum Grouping {
|
||||
/// No grouping is applied
|
||||
None,
|
||||
/// Widgets are grouped as a whole
|
||||
Bar(GroupingConfig),
|
||||
/// Widgets are grouped by alignment
|
||||
Alignment(GroupingConfig),
|
||||
/// Widgets are grouped individually
|
||||
Widget(GroupingConfig),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RenderConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub monitor_idx: usize,
|
||||
/// Spacing between widgets
|
||||
pub spacing: f32,
|
||||
/// Sets how widgets are grouped
|
||||
pub grouping: Grouping,
|
||||
/// Background color
|
||||
pub background_color: Color32,
|
||||
/// Alignment of the widgets
|
||||
pub alignment: Option<Alignment>,
|
||||
/// Add more inner margin when adding a widget group
|
||||
pub more_inner_margin: bool,
|
||||
/// Set to true after the first time the apply_on_widget was called on an alignment
|
||||
pub applied_on_widget: bool,
|
||||
/// FontId for text
|
||||
pub text_font_id: FontId,
|
||||
/// FontId for icon (based on scaling the text font id)
|
||||
pub icon_font_id: FontId,
|
||||
/// Show all icons on the workspace section of the Komorebi widget
|
||||
pub show_all_icons: bool,
|
||||
}
|
||||
|
||||
pub trait RenderExt {
|
||||
fn new_renderconfig(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
background_color: Color32,
|
||||
icon_scale: Option<f32>,
|
||||
) -> RenderConfig;
|
||||
}
|
||||
|
||||
impl RenderExt for &KomobarConfig {
|
||||
fn new_renderconfig(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
background_color: Color32,
|
||||
icon_scale: Option<f32>,
|
||||
) -> RenderConfig {
|
||||
let text_font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut icon_font_id = text_font_id.clone();
|
||||
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
|
||||
|
||||
let monitor_idx = match &self.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
|
||||
MonitorConfigOrIndex::Index(idx) => *idx,
|
||||
};
|
||||
|
||||
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
|
||||
let show_all_icons =
|
||||
KomobarConfig::show_all_icons_on_komorebi_workspace(&self.left_widgets)
|
||||
|| self
|
||||
.center_widgets
|
||||
.as_ref()
|
||||
.is_some_and(|list| KomobarConfig::show_all_icons_on_komorebi_workspace(list))
|
||||
|| KomobarConfig::show_all_icons_on_komorebi_workspace(&self.right_widgets);
|
||||
|
||||
RenderConfig {
|
||||
monitor_idx,
|
||||
spacing: self.widget_spacing.unwrap_or(10.0),
|
||||
grouping: self.grouping.unwrap_or(Grouping::None),
|
||||
background_color,
|
||||
alignment: None,
|
||||
more_inner_margin: false,
|
||||
applied_on_widget: false,
|
||||
text_font_id,
|
||||
icon_font_id,
|
||||
show_all_icons,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderConfig {
|
||||
pub fn load_show_komorebi_layout_options() -> bool {
|
||||
SHOW_KOMOREBI_LAYOUT_OPTIONS.load(Ordering::SeqCst) != 0
|
||||
}
|
||||
|
||||
pub fn store_show_komorebi_layout_options(show: bool) {
|
||||
SHOW_KOMOREBI_LAYOUT_OPTIONS.store(show as usize, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
monitor_idx: 0,
|
||||
spacing: 0.0,
|
||||
grouping: Grouping::None,
|
||||
background_color: Color32::BLACK,
|
||||
alignment: None,
|
||||
more_inner_margin: false,
|
||||
applied_on_widget: false,
|
||||
text_font_id: FontId::default(),
|
||||
icon_font_id: FontId::default(),
|
||||
show_all_icons: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_frame_on_bar(
|
||||
&mut self,
|
||||
frame: Frame,
|
||||
ui_style: &Arc<eframe::egui::Style>,
|
||||
) -> Frame {
|
||||
self.alignment = None;
|
||||
|
||||
if let Grouping::Bar(config) = self.grouping {
|
||||
return self.define_group_frame(
|
||||
//TODO: this outer margin can be a config
|
||||
Some(Margin {
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
}),
|
||||
config,
|
||||
ui_style,
|
||||
);
|
||||
}
|
||||
|
||||
frame
|
||||
}
|
||||
|
||||
pub fn apply_on_alignment<R>(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.alignment = None;
|
||||
|
||||
if let Grouping::Alignment(config) = self.grouping {
|
||||
return self.define_group(None, config, ui, add_contents);
|
||||
}
|
||||
|
||||
Self::fallback_group(ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn apply_on_widget<R>(
|
||||
&mut self,
|
||||
more_inner_margin: bool,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.more_inner_margin = more_inner_margin;
|
||||
let outer_margin = self.widget_outer_margin(ui);
|
||||
|
||||
if let Grouping::Widget(config) = self.grouping {
|
||||
return self.define_group(Some(outer_margin), config, ui, add_contents);
|
||||
}
|
||||
|
||||
self.fallback_widget_group(Some(outer_margin), ui, add_contents)
|
||||
}
|
||||
|
||||
fn fallback_group<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||
InnerResponse {
|
||||
inner: add_contents(ui),
|
||||
response: ui.response().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_widget_group<R>(
|
||||
&mut self,
|
||||
outer_margin: Option<Margin>,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
Frame::NONE
|
||||
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
|
||||
.inner_margin(match self.more_inner_margin {
|
||||
true => Margin::symmetric(5, 0),
|
||||
false => Margin::same(0),
|
||||
})
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
|
||||
fn define_group<R>(
|
||||
&mut self,
|
||||
outer_margin: Option<Margin>,
|
||||
config: GroupingConfig,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.define_group_frame(outer_margin, config, ui.style())
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn define_group_frame(
|
||||
&mut self,
|
||||
outer_margin: Option<Margin>,
|
||||
config: GroupingConfig,
|
||||
ui_style: &Arc<eframe::egui::Style>,
|
||||
) -> Frame {
|
||||
Frame::group(ui_style)
|
||||
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
|
||||
.inner_margin(match self.more_inner_margin {
|
||||
true => Margin::symmetric(6, 1),
|
||||
false => Margin::symmetric(1, 1),
|
||||
})
|
||||
.stroke(ui_style.visuals.widgets.noninteractive.bg_stroke)
|
||||
.corner_radius(match config.rounding {
|
||||
Some(rounding) => rounding.into(),
|
||||
None => ui_style.visuals.widgets.noninteractive.corner_radius,
|
||||
})
|
||||
.fill(
|
||||
self.background_color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
)
|
||||
.shadow(match config.style {
|
||||
Some(style) => match style {
|
||||
// new styles can be added if needed here
|
||||
GroupingStyle::Default => Shadow::NONE,
|
||||
GroupingStyle::DefaultWithShadowB4O1S3 => Shadow {
|
||||
blur: 4,
|
||||
offset: [1, 1],
|
||||
spread: 3,
|
||||
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithShadowB4O0S3 => Shadow {
|
||||
blur: 4,
|
||||
offset: [0, 0],
|
||||
spread: 3,
|
||||
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithShadowB0O1S3 => Shadow {
|
||||
blur: 0,
|
||||
offset: [1, 1],
|
||||
spread: 3,
|
||||
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithGlowB3O1S2 => Shadow {
|
||||
blur: 3,
|
||||
offset: [1, 1],
|
||||
spread: 2,
|
||||
color: ui_style
|
||||
.visuals
|
||||
.selection
|
||||
.stroke
|
||||
.color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithGlowB3O0S2 => Shadow {
|
||||
blur: 3,
|
||||
offset: [0, 0],
|
||||
spread: 2,
|
||||
color: ui_style
|
||||
.visuals
|
||||
.selection
|
||||
.stroke
|
||||
.color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
GroupingStyle::DefaultWithGlowB0O1S2 => Shadow {
|
||||
blur: 0,
|
||||
offset: [1, 1],
|
||||
spread: 2,
|
||||
color: ui_style
|
||||
.visuals
|
||||
.selection
|
||||
.stroke
|
||||
.color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
},
|
||||
None => Shadow::NONE,
|
||||
})
|
||||
}
|
||||
|
||||
fn widget_outer_margin(&mut self, ui: &mut Ui) -> Margin {
|
||||
let spacing = if self.applied_on_widget {
|
||||
// Remove the default item spacing from the margin
|
||||
(self.spacing - ui.spacing().item_spacing.x) as i8
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if !self.applied_on_widget {
|
||||
self.applied_on_widget = true;
|
||||
}
|
||||
|
||||
Margin {
|
||||
left: match self.alignment {
|
||||
Some(align) => match align {
|
||||
Alignment::Left => spacing,
|
||||
Alignment::Center => spacing,
|
||||
Alignment::Right => 0,
|
||||
},
|
||||
None => 0,
|
||||
},
|
||||
right: match self.alignment {
|
||||
Some(align) => match align {
|
||||
Alignment::Left => 0,
|
||||
Alignment::Center => 0,
|
||||
Alignment::Right => spacing,
|
||||
},
|
||||
None => 0,
|
||||
},
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GroupingConfig {
|
||||
/// Styles for the grouping
|
||||
pub style: Option<GroupingStyle>,
|
||||
/// Alpha value for the color transparency [[0-255]] (default: 200)
|
||||
pub transparency_alpha: Option<u8>,
|
||||
/// Rounding values for the 4 corners. Can be a single or 4 values.
|
||||
pub rounding: Option<RoundingConfig>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum GroupingStyle {
|
||||
#[serde(alias = "CtByte")]
|
||||
Default,
|
||||
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
|
||||
#[serde(alias = "CtByteWithShadow")]
|
||||
#[serde(alias = "DefaultWithShadow")]
|
||||
DefaultWithShadowB4O1S3,
|
||||
/// A shadow is added under the default group. (blur: 4, offset: x-0 y-0, spread: 3)
|
||||
DefaultWithShadowB4O0S3,
|
||||
/// A shadow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 3)
|
||||
DefaultWithShadowB0O1S3,
|
||||
/// A glow is added under the default group. (blur: 3, offset: x-1 y-1, spread: 2)
|
||||
DefaultWithGlowB3O1S2,
|
||||
/// A glow is added under the default group. (blur: 3, offset: x-0 y-0, spread: 2)
|
||||
DefaultWithGlowB3O0S2,
|
||||
/// A glow is added under the default group. (blur: 0, offset: x-1 y-1, spread: 2)
|
||||
DefaultWithGlowB0O1S2,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum RoundingConfig {
|
||||
/// All 4 corners are the same
|
||||
Same(f32),
|
||||
/// All 4 corners are custom. Order: NW, NE, SW, SE
|
||||
Individual([f32; 4]),
|
||||
}
|
||||
|
||||
impl From<RoundingConfig> for CornerRadius {
|
||||
fn from(value: RoundingConfig) -> Self {
|
||||
match value {
|
||||
RoundingConfig::Same(value) => Self::same(value as u8),
|
||||
RoundingConfig::Individual(values) => {
|
||||
let values = values.map(|f| f as u8);
|
||||
Self {
|
||||
nw: values[0],
|
||||
ne: values[1],
|
||||
sw: values[2],
|
||||
se: values[3],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Color32Ext {
|
||||
fn try_apply_alpha(self, transparency_alpha: Option<u8>) -> Self;
|
||||
}
|
||||
|
||||
impl Color32Ext for Color32 {
|
||||
/// Tries to apply the alpha value to the Color32
|
||||
fn try_apply_alpha(self, transparency_alpha: Option<u8>) -> Self {
|
||||
if let Some(alpha) = transparency_alpha {
|
||||
return Color32::from_rgba_unmultiplied(self.r(), self.g(), self.b(), alpha);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
use eframe::egui::CursorIcon;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Response;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Ui;
|
||||
|
||||
/// Same as SelectableLabel, but supports all content
|
||||
pub struct SelectableFrame {
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl SelectableFrame {
|
||||
pub fn new(selected: bool) -> Self {
|
||||
Self { selected }
|
||||
}
|
||||
|
||||
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
|
||||
let Self { selected } = self;
|
||||
|
||||
Frame::NONE
|
||||
.show(ui, |ui| {
|
||||
let response = ui.interact(ui.max_rect(), ui.unique_id(), Sense::click());
|
||||
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
let inner_margin = Margin::symmetric(
|
||||
ui.style().spacing.button_padding.x as i8,
|
||||
ui.style().spacing.button_padding.y as i8,
|
||||
);
|
||||
|
||||
if selected
|
||||
|| response.hovered()
|
||||
|| response.highlighted()
|
||||
|| response.has_focus()
|
||||
{
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
Frame::NONE
|
||||
.stroke(visuals.bg_stroke)
|
||||
.corner_radius(visuals.corner_radius)
|
||||
.fill(visuals.bg_fill)
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
} else {
|
||||
Frame::NONE
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
})
|
||||
.inner
|
||||
.on_hover_cursor(CursorIcon::PointingHand)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
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;
|
||||
@@ -50,7 +51,7 @@ 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(true);
|
||||
self.disks.refresh();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
@@ -78,8 +79,15 @@ impl Storage {
|
||||
}
|
||||
|
||||
impl BarWidget for Storage {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 {
|
||||
@@ -88,7 +96,7 @@ impl BarWidget for Storage {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -96,31 +104,30 @@ impl BarWidget for Storage {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
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()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::Align;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::epaint::StrokeKind;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -42,16 +37,8 @@ impl From<TimeConfig> for Time {
|
||||
pub enum TimeFormat {
|
||||
/// Twelve-hour format (with seconds)
|
||||
TwelveHour,
|
||||
/// Twelve-hour format (without seconds)
|
||||
TwelveHourWithoutSeconds,
|
||||
/// Twenty-four-hour format (with seconds)
|
||||
TwentyFourHour,
|
||||
/// Twenty-four-hour format (without seconds)
|
||||
TwentyFourHourWithoutSeconds,
|
||||
/// Twenty-four-hour format displayed as a binary clock with circles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
|
||||
BinaryCircle,
|
||||
/// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
|
||||
BinaryRectangle,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
Custom(String),
|
||||
}
|
||||
@@ -59,12 +46,8 @@ pub enum TimeFormat {
|
||||
impl TimeFormat {
|
||||
pub fn toggle(&mut self) {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => *self = TimeFormat::TwelveHourWithoutSeconds,
|
||||
TimeFormat::TwelveHourWithoutSeconds => *self = TimeFormat::TwentyFourHour,
|
||||
TimeFormat::TwentyFourHour => *self = TimeFormat::TwentyFourHourWithoutSeconds,
|
||||
TimeFormat::TwentyFourHourWithoutSeconds => *self = TimeFormat::BinaryCircle,
|
||||
TimeFormat::BinaryCircle => *self = TimeFormat::BinaryRectangle,
|
||||
TimeFormat::BinaryRectangle => *self = TimeFormat::TwelveHour,
|
||||
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
|
||||
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
@@ -72,11 +55,7 @@ impl TimeFormat {
|
||||
fn fmt_string(&self) -> String {
|
||||
match self {
|
||||
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
|
||||
TimeFormat::TwelveHourWithoutSeconds => String::from("%l:%M %p"),
|
||||
TimeFormat::TwentyFourHour => String::from("%T"),
|
||||
TimeFormat::TwentyFourHourWithoutSeconds => String::from("%H:%M"),
|
||||
TimeFormat::BinaryCircle => String::from("c%T"),
|
||||
TimeFormat::BinaryRectangle => String::from("r%T"),
|
||||
TimeFormat::Custom(format) => format.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -94,184 +73,20 @@ impl Time {
|
||||
chrono::Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn paint_binary_circle(
|
||||
&mut self,
|
||||
size: f32,
|
||||
number: u32,
|
||||
max_power: usize,
|
||||
ctx: &Context,
|
||||
ui: &mut Ui,
|
||||
) {
|
||||
let full_height = size;
|
||||
let height = full_height / 4.0;
|
||||
let width = height;
|
||||
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::new(width, full_height), Sense::hover());
|
||||
let color = ctx.style().visuals.text_color();
|
||||
|
||||
let c = response.rect.center();
|
||||
let r = height / 2.0 - 0.5;
|
||||
|
||||
if number == 1 || number == 3 || number == 5 || number == 7 || number == 9 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 1.50), r, color);
|
||||
} else {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 1.50), r / 2.5, color);
|
||||
}
|
||||
|
||||
if number == 2 || number == 3 || number == 6 || number == 7 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 0.50), r, color);
|
||||
} else {
|
||||
painter.circle_filled(c + Vec2::new(0.0, height * 0.50), r / 2.5, color);
|
||||
}
|
||||
|
||||
if number == 4 || number == 5 || number == 6 || number == 7 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50), r, color);
|
||||
} else if max_power > 2 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50), r / 2.5, color);
|
||||
}
|
||||
|
||||
if number == 8 || number == 9 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50), r, color);
|
||||
} else if max_power > 3 {
|
||||
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50), r / 2.5, color);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_binary_rect(
|
||||
&mut self,
|
||||
size: f32,
|
||||
number: u32,
|
||||
max_power: usize,
|
||||
ctx: &Context,
|
||||
ui: &mut Ui,
|
||||
) {
|
||||
let full_height = size;
|
||||
let height = full_height / 4.0;
|
||||
let width = height * 1.5;
|
||||
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::new(width, full_height), Sense::hover());
|
||||
let color = ctx.style().visuals.text_color();
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
|
||||
let round_all = CornerRadius::same((response.rect.width() * 0.1) as u8);
|
||||
let round_top = CornerRadius {
|
||||
nw: round_all.nw,
|
||||
ne: round_all.ne,
|
||||
..Default::default()
|
||||
};
|
||||
let round_none = CornerRadius::ZERO;
|
||||
let round_bottom = CornerRadius {
|
||||
sw: round_all.nw,
|
||||
se: round_all.ne,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if max_power == 2 {
|
||||
let mut rect = response.rect.shrink(stroke.width);
|
||||
rect.set_height(rect.height() - height * 2.0);
|
||||
rect = rect.translate(Vec2::new(0.0, height * 2.0));
|
||||
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
|
||||
} else if max_power == 3 {
|
||||
let mut rect = response.rect.shrink(stroke.width);
|
||||
rect.set_height(rect.height() - height);
|
||||
rect = rect.translate(Vec2::new(0.0, height));
|
||||
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
|
||||
} else {
|
||||
painter.rect_stroke(
|
||||
response.rect.shrink(stroke.width),
|
||||
round_all,
|
||||
stroke,
|
||||
StrokeKind::Outside,
|
||||
);
|
||||
}
|
||||
|
||||
let mut rect_bin = response.rect;
|
||||
rect_bin.set_width(width);
|
||||
|
||||
if number == 1 || number == 5 || number == 9 {
|
||||
rect_bin.set_height(height);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height * 3.0)),
|
||||
round_bottom,
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 2 {
|
||||
rect_bin.set_height(height);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height * 2.0)),
|
||||
if max_power == 2 {
|
||||
round_top
|
||||
} else {
|
||||
round_none
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 3 {
|
||||
rect_bin.set_height(height * 2.0);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height * 2.0)),
|
||||
round_bottom,
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 4 || number == 5 {
|
||||
rect_bin.set_height(height);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height * 1.0)),
|
||||
if max_power == 3 {
|
||||
round_top
|
||||
} else {
|
||||
round_none
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 6 {
|
||||
rect_bin.set_height(height * 2.0);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height * 1.0)),
|
||||
if max_power == 3 {
|
||||
round_top
|
||||
} else {
|
||||
round_none
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 7 {
|
||||
rect_bin.set_height(height * 3.0);
|
||||
painter.rect_filled(
|
||||
rect_bin.translate(Vec2::new(0.0, height)),
|
||||
if max_power == 3 {
|
||||
round_all
|
||||
} else {
|
||||
round_bottom
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
if number == 8 || number == 9 {
|
||||
rect_bin.set_height(height);
|
||||
painter.rect_filled(rect_bin.translate(Vec2::new(0.0, 0.0)), round_top, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Time {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.enable {
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let use_binary_circle = output.starts_with('c');
|
||||
let use_binary_rectangle = output.starts_with('r');
|
||||
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 {
|
||||
@@ -280,7 +95,7 @@ impl BarWidget for Time {
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
@@ -289,89 +104,25 @@ impl BarWidget for Time {
|
||||
output.insert_str(0, "TIME: ");
|
||||
}
|
||||
|
||||
if !use_binary_circle && !use_binary_rectangle {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
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()
|
||||
}
|
||||
|
||||
let font_id = config.icon_font_id.clone();
|
||||
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
if !is_reversed {
|
||||
ui.add(Label::new(layout_job.clone()).selectable(false));
|
||||
}
|
||||
|
||||
if use_binary_circle || use_binary_rectangle {
|
||||
let ordered_output = if is_reversed {
|
||||
output.chars().rev().collect()
|
||||
} else {
|
||||
output
|
||||
};
|
||||
|
||||
for (section_index, section) in
|
||||
ordered_output.split(':').enumerate()
|
||||
{
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing = Vec2::splat(2.0);
|
||||
for (number_index, number_char) in
|
||||
section.chars().enumerate()
|
||||
{
|
||||
if let Some(number) = number_char.to_digit(10) {
|
||||
// the hour is the second char in the first section (in reverse, it's in the last section)
|
||||
let max_power = match (
|
||||
is_reversed,
|
||||
section_index,
|
||||
number_index,
|
||||
) {
|
||||
(true, 2, 1) | (false, 0, 1) => 2,
|
||||
(true, _, 1) | (false, _, 0) => 3,
|
||||
_ => 4,
|
||||
};
|
||||
|
||||
if use_binary_circle {
|
||||
self.paint_binary_circle(
|
||||
font_id.size,
|
||||
number,
|
||||
max_power,
|
||||
ctx,
|
||||
ui,
|
||||
);
|
||||
} else if use_binary_rectangle {
|
||||
self.paint_binary_rect(
|
||||
font_id.size,
|
||||
number,
|
||||
max_power,
|
||||
ctx,
|
||||
ui,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if is_reversed {
|
||||
ui.add(Label::new(layout_job.clone()).selectable(false));
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
self.format.toggle()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct UpdateConfig {
|
||||
/// Enable the Update widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 12 hours)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<UpdateConfig> for Update {
|
||||
fn from(value: UpdateConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(12);
|
||||
|
||||
let mut latest_version = String::new();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
if let Ok(response) = client
|
||||
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
|
||||
.header("User-Agent", "komorebi-bar-version-checker")
|
||||
.send()
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Release {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
if let Ok(release) =
|
||||
serde_json::from_str::<Release>(&response.text().unwrap_or_default())
|
||||
{
|
||||
let trimmed = release.tag_name.trim_start_matches("v");
|
||||
latest_version = trimmed.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
data_refresh_interval,
|
||||
installed_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
latest_version,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Update {
|
||||
pub enable: bool,
|
||||
data_refresh_interval: u64,
|
||||
installed_version: String,
|
||||
latest_version: String,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Update {
|
||||
fn output(&mut self) -> String {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated)
|
||||
> Duration::from_secs((self.data_refresh_interval * 60) * 60)
|
||||
{
|
||||
let client = reqwest::blocking::Client::new();
|
||||
if let Ok(response) = client
|
||||
.get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest")
|
||||
.header("User-Agent", "komorebi-bar-version-checker")
|
||||
.send()
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Release {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
if let Ok(release) =
|
||||
serde_json::from_str::<Release>(&response.text().unwrap_or_default())
|
||||
{
|
||||
let trimmed = release.tag_name.trim_start_matches("v");
|
||||
self.latest_version = trimmed.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
if self.latest_version > self.installed_version {
|
||||
format!("Update available! v{}", self.latest_version)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Update {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ROCKET_LAUNCH.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("explorer.exe")
|
||||
.args([format!(
|
||||
"https://github.com/LGUG2Z/komorebi/releases/v{}",
|
||||
self.latest_version
|
||||
)])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ use crate::cpu::Cpu;
|
||||
use crate::cpu::CpuConfig;
|
||||
use crate::date::Date;
|
||||
use crate::date::DateConfig;
|
||||
use crate::keyboard::Keyboard;
|
||||
use crate::keyboard::KeyboardConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiConfig;
|
||||
use crate::media::Media;
|
||||
@@ -14,13 +12,10 @@ use crate::memory::Memory;
|
||||
use crate::memory::MemoryConfig;
|
||||
use crate::network::Network;
|
||||
use crate::network::NetworkConfig;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::storage::Storage;
|
||||
use crate::storage::StorageConfig;
|
||||
use crate::time::Time;
|
||||
use crate::time::TimeConfig;
|
||||
use crate::update::Update;
|
||||
use crate::update::UpdateConfig;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
@@ -28,7 +23,7 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub trait BarWidget {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig);
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -36,14 +31,12 @@ pub enum WidgetConfig {
|
||||
Battery(BatteryConfig),
|
||||
Cpu(CpuConfig),
|
||||
Date(DateConfig),
|
||||
Keyboard(KeyboardConfig),
|
||||
Komorebi(KomorebiConfig),
|
||||
Media(MediaConfig),
|
||||
Memory(MemoryConfig),
|
||||
Network(NetworkConfig),
|
||||
Storage(StorageConfig),
|
||||
Time(TimeConfig),
|
||||
Update(UpdateConfig),
|
||||
}
|
||||
|
||||
impl WidgetConfig {
|
||||
@@ -52,38 +45,12 @@ impl WidgetConfig {
|
||||
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::Keyboard(config) => Box::new(Keyboard::from(*config)),
|
||||
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())),
|
||||
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
match self {
|
||||
WidgetConfig::Battery(config) => config.enable,
|
||||
WidgetConfig::Cpu(config) => config.enable,
|
||||
WidgetConfig::Date(config) => config.enable,
|
||||
WidgetConfig::Keyboard(config) => config.enable,
|
||||
WidgetConfig::Komorebi(config) => {
|
||||
config.workspaces.as_ref().is_some_and(|w| w.enable)
|
||||
|| config.layout.as_ref().is_some_and(|w| w.enable)
|
||||
|| config.focused_window.as_ref().is_some_and(|w| w.enable)
|
||||
|| config
|
||||
.configuration_switcher
|
||||
.as_ref()
|
||||
.is_some_and(|w| w.enable)
|
||||
}
|
||||
WidgetConfig::Media(config) => config.enable,
|
||||
WidgetConfig::Memory(config) => config.enable,
|
||||
WidgetConfig::Network(config) => config.enable,
|
||||
WidgetConfig::Storage(config) => config.enable,
|
||||
WidgetConfig::Time(config) => config.enable,
|
||||
WidgetConfig::Update(config) => config.enable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.35"
|
||||
edition = "2024"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
@@ -1,46 +1,18 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::AnimationsConfig;
|
||||
pub use komorebi::AspectRatio;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::CrossBoundaryBehaviour;
|
||||
pub use komorebi::GlobalState;
|
||||
pub use komorebi::KomorebiTheme;
|
||||
pub use komorebi::MonitorConfig;
|
||||
pub use komorebi::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::PredefinedAspectRatio;
|
||||
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::WindowContainerBehaviour;
|
||||
pub use komorebi::WindowsApi;
|
||||
pub use komorebi::WorkspaceConfig;
|
||||
pub use komorebi::animation::PerAnimationPrefixConfig;
|
||||
pub use komorebi::animation::prefix::AnimationPrefix;
|
||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||
pub use komorebi::colour::Colour;
|
||||
pub use komorebi::colour::Rgb;
|
||||
pub use komorebi::config_generation::ApplicationConfiguration;
|
||||
pub use komorebi::config_generation::IdWithIdentifier;
|
||||
pub use komorebi::config_generation::IdWithIdentifierAndComment;
|
||||
pub use komorebi::config_generation::MatchingRule;
|
||||
pub use komorebi::config_generation::MatchingStrategy;
|
||||
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::Column;
|
||||
pub use komorebi::core::ColumnSplit;
|
||||
pub use komorebi::core::ColumnSplitWithCapacity;
|
||||
pub use komorebi::core::ColumnWidth;
|
||||
pub use komorebi::core::CustomLayout;
|
||||
pub use komorebi::core::CycleDirection;
|
||||
pub use komorebi::core::DefaultLayout;
|
||||
@@ -51,7 +23,6 @@ pub use komorebi::core::Layout;
|
||||
pub use komorebi::core::MoveBehaviour;
|
||||
pub use komorebi::core::OperationBehaviour;
|
||||
pub use komorebi::core::OperationDirection;
|
||||
pub use komorebi::core::PathExt;
|
||||
pub use komorebi::core::Rect;
|
||||
pub use komorebi::core::Sizing;
|
||||
pub use komorebi::core::SocketMessage;
|
||||
@@ -59,16 +30,22 @@ pub use komorebi::core::StackbarLabel;
|
||||
pub use komorebi::core::StackbarMode;
|
||||
pub use komorebi::core::StateQuery;
|
||||
pub use komorebi::core::WindowKind;
|
||||
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
||||
pub use komorebi::core::resolve_home_path;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
pub use komorebi::monitor_reconciliator::MonitorNotification;
|
||||
pub use komorebi::ring::Ring;
|
||||
pub use komorebi::window::Window;
|
||||
pub use komorebi::window_manager_event::WindowManagerEvent;
|
||||
pub use komorebi::workspace::Workspace;
|
||||
pub use komorebi::workspace::WorkspaceGlobals;
|
||||
pub use komorebi::workspace::WorkspaceLayer;
|
||||
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;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
@@ -76,7 +53,6 @@ use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::Shutdown;
|
||||
use std::time::Duration;
|
||||
pub use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
@@ -85,30 +61,13 @@ const KOMOREBI: &str = "komorebi.sock";
|
||||
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
|
||||
if let Ok(m_str) = serde_json::to_string(&m) {
|
||||
s.push_str(&m_str);
|
||||
s.push('\n');
|
||||
}
|
||||
s
|
||||
});
|
||||
stream.write_all(msgs.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_read_timeout(Some(Duration::from_secs(1)))?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
|
||||
stream.shutdown(Shutdown::Write)?;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.35"
|
||||
edition = "2024"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -10,7 +10,6 @@ komorebi-client = { path = "../komorebi-client" }
|
||||
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
random_word = { version = "0.4", features = ["en"] }
|
||||
random_word = { version = "0.4.3", features = ["en"] }
|
||||
serde_json = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
@@ -1,9 +1,9 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
use eframe::egui;
|
||||
use eframe::egui::color_picker::Alpha;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use eframe::egui::color_picker::Alpha;
|
||||
use komorebi_client::BorderStyle;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::DefaultLayout;
|
||||
@@ -101,7 +101,7 @@ impl From<&komorebi_client::Workspace> for WorkspaceConfig {
|
||||
let name = value
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| random_word::r#gen(random_word::Lang::En).to_string());
|
||||
.unwrap_or_else(|| random_word::gen(random_word::Lang::En).to_string());
|
||||
|
||||
Self {
|
||||
layout,
|
||||
@@ -215,7 +215,7 @@ impl KomorebiGui {
|
||||
extern "system" fn enum_window(
|
||||
hwnd: windows::Win32::Foundation::HWND,
|
||||
lparam: windows::Win32::Foundation::LPARAM,
|
||||
) -> windows_core::BOOL {
|
||||
) -> windows::Win32::Foundation::BOOL {
|
||||
let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
|
||||
let window = Window::from(hwnd.0 as isize);
|
||||
|
||||
@@ -248,11 +248,10 @@ impl eframe::App for KomorebiGui {
|
||||
ui.collapsing("Window Rules", |ui| {
|
||||
let window = Window::from(self.debug_hwnd);
|
||||
|
||||
let label = match (window.title(), window.exe()) {
|
||||
(Ok(title), Ok(exe)) => {
|
||||
format!("{title} ({exe})")
|
||||
}
|
||||
_ => String::from("Select a Window"),
|
||||
let label = if let (Ok(title), Ok(exe)) = (window.title(), window.exe()) {
|
||||
format!("{title} ({exe})")
|
||||
} else {
|
||||
String::from("Select a Window")
|
||||
};
|
||||
|
||||
if ui.button("Refresh Windows").clicked() {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.35"
|
||||
edition = "2024"
|
||||
version = "0.1.30"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "96f26c88d83781f234d42222293ec73d23a39ad8" }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "bdaff30959512c4f7ee7304117076a48633d777f", default-features = false, features = ["egui31"] }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
|
||||
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 }
|
||||
serde_variant = "0.1"
|
||||
strum = { workspace = true }
|
||||
|
||||
@@ -4,15 +4,12 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub use base16_egui_themes::Base16;
|
||||
pub use catppuccin_egui;
|
||||
pub use eframe::egui::Color32;
|
||||
use serde_variant::to_variant_name;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Theme {
|
||||
/// A theme from catppuccin-egui
|
||||
@@ -27,29 +24,7 @@ pub enum Theme {
|
||||
},
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn variant_names(&self) -> Vec<String> {
|
||||
match self {
|
||||
Theme::Catppuccin { .. } => {
|
||||
vec![
|
||||
"Frappe".to_string(),
|
||||
"Latte".to_string(),
|
||||
"Macchiato".to_string(),
|
||||
"Mocha".to_string(),
|
||||
]
|
||||
}
|
||||
Theme::Base16 { .. } => Base16::iter()
|
||||
.map(|variant| {
|
||||
to_variant_name(&variant)
|
||||
.expect("could not convert to variant name")
|
||||
.to_string()
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Base16Value {
|
||||
Base00,
|
||||
Base01,
|
||||
@@ -93,7 +68,7 @@ impl Base16Value {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Catppuccin {
|
||||
Frappe,
|
||||
Latte,
|
||||
@@ -118,7 +93,7 @@ impl From<Catppuccin> for catppuccin_egui::Theme {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum CatppuccinValue {
|
||||
Rosewater,
|
||||
Flamingo,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.35"
|
||||
version = "0.1.30"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -25,7 +28,7 @@ lazy_static = { workspace = true }
|
||||
miow = "0.6"
|
||||
nanoid = "0.4"
|
||||
net2 = "0.2"
|
||||
os_info = "3.10"
|
||||
os_info = "3.8"
|
||||
parking_lot = "0.12"
|
||||
paste = { workspace = true }
|
||||
regex = "1"
|
||||
@@ -34,27 +37,24 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
shadow-rs = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
sysinfo = { workspace = true }
|
||||
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-numerics = { workspace = true }
|
||||
windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.55"
|
||||
winreg = "0.52"
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
|
||||
[features]
|
||||
deadlock_detection = ["parking_lot/deadlock_detection"]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use shadow_rs::ShadowBuilder;
|
||||
|
||||
fn main() {
|
||||
ShadowBuilder::builder().build().unwrap();
|
||||
shadow_rs::new().unwrap();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
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;
|
||||
@@ -354,8 +370,9 @@ impl Ease for EaseInOutBounce {
|
||||
}
|
||||
}
|
||||
}
|
||||
fn apply_ease_func(t: f64) -> f64 {
|
||||
let style = *ANIMATION_STYLE.lock();
|
||||
|
||||
pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
match style {
|
||||
AnimationStyle::Linear => Linear::evaluate(t),
|
||||
AnimationStyle::EaseInSine => EaseInSine::evaluate(t),
|
||||
@@ -389,3 +406,112 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Animation {
|
||||
pub hwnd: isize,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(hwnd: isize) -> Self {
|
||||
Self { hwnd }
|
||||
}
|
||||
|
||||
/// Returns true if the animation needs to continue
|
||||
pub fn cancel(&mut self) -> bool {
|
||||
if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// should be more than 0
|
||||
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(self.hwnd);
|
||||
let max_duration = Duration::from_secs(1);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(
|
||||
ANIMATION_DURATION.load(Ordering::SeqCst) / 2,
|
||||
));
|
||||
}
|
||||
|
||||
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(self.hwnd);
|
||||
|
||||
ANIMATION_MANAGER.lock().end_cancel(self.hwnd);
|
||||
|
||||
latest_cancel_idx == cancel_idx
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn lerp(start: i32, end: i32, t: f64) -> i32 {
|
||||
let time = apply_ease_func(t);
|
||||
f64::from(end - start)
|
||||
.mul_add(time, f64::from(start))
|
||||
.round() as i32
|
||||
}
|
||||
|
||||
pub fn lerp_rect(start_rect: &Rect, end_rect: &Rect, t: f64) -> Rect {
|
||||
Rect {
|
||||
left: Self::lerp(start_rect.left, end_rect.left, t),
|
||||
top: Self::lerp(start_rect.top, end_rect.top, t),
|
||||
right: Self::lerp(start_rect.right, end_rect.right, t),
|
||||
bottom: Self::lerp(start_rect.bottom, end_rect.bottom, t),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
&mut self,
|
||||
duration: Duration,
|
||||
mut render_callback: impl FnMut(f64) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
if ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
let should_animate = self.cancel();
|
||||
|
||||
if !should_animate {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().start(self.hwnd);
|
||||
|
||||
let target_frame_time = Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
|
||||
let mut progress = 0.0;
|
||||
let animation_start = Instant::now();
|
||||
|
||||
// start animation
|
||||
while progress < 1.0 {
|
||||
// check if animation is cancelled
|
||||
if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) {
|
||||
// cancel animation
|
||||
ANIMATION_MANAGER.lock().cancel(self.hwnd);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame_start = Instant::now();
|
||||
// calculate progress
|
||||
progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
|
||||
render_callback(progress).ok();
|
||||
|
||||
// sleep until next frame
|
||||
let frame_time_elapsed = frame_start.elapsed();
|
||||
|
||||
if frame_time_elapsed < target_frame_time {
|
||||
std::thread::sleep(target_frame_time - frame_time_elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
|
||||
// limit progress to 1.0 if animation took longer
|
||||
if progress > 1.0 {
|
||||
progress = 1.0;
|
||||
}
|
||||
|
||||
// process animation for 1.0 to set target position
|
||||
render_callback(progress)
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use super::prefix::AnimationPrefix;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct AnimationState {
|
||||
pub in_progress: bool,
|
||||
pub cancel_idx_counter: usize,
|
||||
pub pending_cancel_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AnimationManager {
|
||||
animations: HashMap<String, AnimationState>,
|
||||
}
|
||||
|
||||
impl Default for AnimationManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
animations: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self, animation_key: &str) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(animation_key) {
|
||||
animation_state.pending_cancel_count > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_progress(&self, animation_key: &str) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(animation_key) {
|
||||
animation_state.in_progress
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_cancel(&mut self, animation_key: &str) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.pending_cancel_count += 1;
|
||||
animation_state.cancel_idx_counter += 1;
|
||||
|
||||
// return cancel idx
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn latest_cancel_idx(&mut self, animation_key: &str) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_cancel(&mut self, animation_key: &str) {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.pending_cancel_count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, animation_key: &str) {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.in_progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, animation_key: &str) {
|
||||
if let Entry::Vacant(e) = self.animations.entry(animation_key.to_string()) {
|
||||
e.insert(AnimationState {
|
||||
in_progress: true,
|
||||
cancel_idx_counter: 0,
|
||||
pending_cancel_count: 0,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.in_progress = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&mut self, animation_key: &str) {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.in_progress = false;
|
||||
|
||||
if animation_state.pending_cancel_count == 0 {
|
||||
self.animations.remove(animation_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count_in_progress(&self, animation_key_prefix: AnimationPrefix) -> usize {
|
||||
self.animations
|
||||
.keys()
|
||||
.filter(|key| key.starts_with(animation_key_prefix.to_string().as_str()))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.animations.len()
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::ANIMATION_DURATION_GLOBAL;
|
||||
use super::ANIMATION_FPS;
|
||||
use super::ANIMATION_MANAGER;
|
||||
use super::RenderDispatcher;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct AnimationEngine;
|
||||
|
||||
impl AnimationEngine {
|
||||
pub fn wait_for_all_animations() {
|
||||
let max_duration = Duration::from_secs(20);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().count() > 0 {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
break;
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(
|
||||
ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the animation needs to continue
|
||||
pub fn cancel(animation_key: &str) -> bool {
|
||||
// should be more than 0
|
||||
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(animation_key);
|
||||
let max_duration = Duration::from_secs(5);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().in_progress(animation_key) {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
ANIMATION_MANAGER.lock().end(animation_key);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(250 / 2));
|
||||
}
|
||||
|
||||
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(animation_key);
|
||||
|
||||
ANIMATION_MANAGER.lock().end_cancel(animation_key);
|
||||
|
||||
latest_cancel_idx == cancel_idx
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
render_dispatcher: (impl RenderDispatcher + Send + 'static),
|
||||
duration: Duration,
|
||||
) -> Result<()> {
|
||||
std::thread::spawn(move || {
|
||||
let animation_key = render_dispatcher.get_animation_key();
|
||||
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
|
||||
let should_animate = Self::cancel(animation_key.as_str());
|
||||
|
||||
if !should_animate {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
render_dispatcher.pre_render()?;
|
||||
|
||||
ANIMATION_MANAGER.lock().start(animation_key.as_str());
|
||||
|
||||
let target_frame_time =
|
||||
Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
|
||||
let mut progress = 0.0;
|
||||
let animation_start = Instant::now();
|
||||
|
||||
// start animation
|
||||
while progress < 1.0 {
|
||||
// check if animation is cancelled
|
||||
if ANIMATION_MANAGER
|
||||
.lock()
|
||||
.is_cancelled(animation_key.as_str())
|
||||
{
|
||||
// cancel animation
|
||||
ANIMATION_MANAGER.lock().cancel(animation_key.as_str());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame_start = Instant::now();
|
||||
// calculate progress
|
||||
progress =
|
||||
animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
|
||||
render_dispatcher.render(progress).ok();
|
||||
|
||||
// sleep until next frame
|
||||
let frame_time_elapsed = frame_start.elapsed();
|
||||
|
||||
if frame_time_elapsed < target_frame_time {
|
||||
std::thread::sleep(target_frame_time - frame_time_elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().end(animation_key.as_str());
|
||||
|
||||
// limit progress to 1.0 if animation took longer
|
||||
if progress != 1.0 {
|
||||
progress = 1.0;
|
||||
|
||||
// process animation for 1.0 to set target position
|
||||
render_dispatcher.render(progress).ok();
|
||||
}
|
||||
|
||||
render_dispatcher.post_render()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
use crate::AnimationStyle;
|
||||
use crate::core::Rect;
|
||||
|
||||
use super::style::apply_ease_func;
|
||||
|
||||
pub trait Lerp<T = Self> {
|
||||
fn lerp(self, end: T, time: f64, style: AnimationStyle) -> T;
|
||||
}
|
||||
|
||||
impl Lerp for i32 {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn lerp(self, end: i32, time: f64, style: AnimationStyle) -> i32 {
|
||||
let time = apply_ease_func(time, style);
|
||||
|
||||
f64::from(end - self).mul_add(time, f64::from(self)).round() as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for f64 {
|
||||
fn lerp(self, end: f64, time: f64, style: AnimationStyle) -> f64 {
|
||||
let time = apply_ease_func(time, style);
|
||||
|
||||
(end - self).mul_add(time, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for u8 {
|
||||
fn lerp(self, end: u8, time: f64, style: AnimationStyle) -> u8 {
|
||||
(self as f64).lerp(end as f64, time, style) as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for Rect {
|
||||
fn lerp(self, end: Rect, time: f64, style: AnimationStyle) -> Rect {
|
||||
Rect {
|
||||
left: self.left.lerp(end.left, time, style),
|
||||
top: self.top.lerp(end.top, time, style),
|
||||
right: self.right.lerp(end.right, time, style),
|
||||
bottom: self.bottom.lerp(end.bottom, time, style),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use crate::animation::animation_manager::AnimationManager;
|
||||
use crate::core::animation::AnimationStyle;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use prefix::AnimationPrefix;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
pub use engine::AnimationEngine;
|
||||
pub mod animation_manager;
|
||||
pub mod engine;
|
||||
pub mod lerp;
|
||||
pub mod prefix;
|
||||
pub mod render_dispatcher;
|
||||
pub use render_dispatcher::RenderDispatcher;
|
||||
pub mod style;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PerAnimationPrefixConfig<T> {
|
||||
Prefix(HashMap<AnimationPrefix, T>),
|
||||
Global(T),
|
||||
}
|
||||
|
||||
pub const DEFAULT_ANIMATION_ENABLED: bool = false;
|
||||
pub const DEFAULT_ANIMATION_STYLE: AnimationStyle = AnimationStyle::Linear;
|
||||
pub const DEFAULT_ANIMATION_DURATION: u64 = 250;
|
||||
pub const DEFAULT_ANIMATION_FPS: u64 = 60;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
|
||||
Arc::new(Mutex::new(AnimationManager::new()));
|
||||
pub static ref ANIMATION_STYLE_GLOBAL: Arc<Mutex<AnimationStyle>> =
|
||||
Arc::new(Mutex::new(DEFAULT_ANIMATION_STYLE));
|
||||
pub static ref ANIMATION_ENABLED_GLOBAL: Arc<AtomicBool> =
|
||||
Arc::new(AtomicBool::new(DEFAULT_ANIMATION_ENABLED));
|
||||
pub static ref ANIMATION_DURATION_GLOBAL: Arc<AtomicU64> =
|
||||
Arc::new(AtomicU64::new(DEFAULT_ANIMATION_DURATION));
|
||||
pub static ref ANIMATION_STYLE_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, AnimationStyle>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref ANIMATION_ENABLED_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, bool>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref ANIMATION_DURATION_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, u64>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(DEFAULT_ANIMATION_FPS);
|
||||
@@ -1,31 +0,0 @@
|
||||
use clap::ValueEnum;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AnimationPrefix {
|
||||
Movement,
|
||||
Transparency,
|
||||
}
|
||||
|
||||
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
|
||||
format!("{}:{}", prefix, key)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
pub trait RenderDispatcher {
|
||||
fn get_animation_key(&self) -> String;
|
||||
fn pre_render(&self) -> Result<()>;
|
||||
fn render(&self, delta: f64) -> Result<()>;
|
||||
fn post_render(&self) -> Result<()>;
|
||||
}
|
||||
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,102 +1,49 @@
|
||||
use crate::WINDOWS_11;
|
||||
use crate::WindowsApi;
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::FOCUS_STATE;
|
||||
use crate::border_manager::RENDER_TARGETS;
|
||||
use crate::border_manager::RenderTarget;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
use crate::windows_api;
|
||||
use color_eyre::eyre::anyhow;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use windows::Win32::Foundation::FALSE;
|
||||
use std::time::Duration;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::TRUE;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_HWND_RENDER_TARGET_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_PRESENT_OPTIONS_IMMEDIATELY;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
|
||||
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
|
||||
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
|
||||
use windows::Win32::Graphics::Gdi::CreateRectRgn;
|
||||
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::ValidateRect;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::RoundRect;
|
||||
use windows::Win32::Graphics::Gdi::SelectObject;
|
||||
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
|
||||
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows_core::BOOL;
|
||||
use windows_core::PCWSTR;
|
||||
use windows_numerics::Matrix3x2;
|
||||
|
||||
pub struct RenderFactory(ID2D1Factory);
|
||||
unsafe impl Sync for RenderFactory {}
|
||||
unsafe impl Send for RenderFactory {}
|
||||
|
||||
impl Deref for RenderFactory {
|
||||
type Target = ID2D1Factory;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
static RENDER_FACTORY: LazyLock<RenderFactory> = unsafe {
|
||||
LazyLock::new(|| {
|
||||
RenderFactory(
|
||||
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
|
||||
.expect("creating RENDER_FACTORY failed"),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
static BRUSH_PROPERTIES: LazyLock<D2D1_BRUSH_PROPERTIES> =
|
||||
LazyLock::new(|| D2D1_BRUSH_PROPERTIES {
|
||||
opacity: 1.0,
|
||||
transform: Matrix3x2::identity(),
|
||||
});
|
||||
|
||||
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||
@@ -111,36 +58,14 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
true.into()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct Border {
|
||||
pub hwnd: isize,
|
||||
pub render_target: OnceLock<RenderTarget>,
|
||||
pub tracking_hwnd: isize,
|
||||
pub window_rect: Rect,
|
||||
pub window_kind: WindowKind,
|
||||
pub style: BorderStyle,
|
||||
pub width: i32,
|
||||
pub offset: i32,
|
||||
pub brush_properties: D2D1_BRUSH_PROPERTIES,
|
||||
pub rounded_rect: D2D1_ROUNDED_RECT,
|
||||
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
|
||||
}
|
||||
|
||||
impl From<isize> for Border {
|
||||
fn from(value: isize) -> Self {
|
||||
Self {
|
||||
hwnd: value,
|
||||
render_target: OnceLock::new(),
|
||||
tracking_hwnd: 0,
|
||||
window_rect: Rect::default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
style: STYLE.load(),
|
||||
width: BORDER_WIDTH.load(Ordering::Relaxed),
|
||||
offset: BORDER_OFFSET.load(Ordering::Relaxed),
|
||||
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
|
||||
rounded_rect: D2D1_ROUNDED_RECT::default(),
|
||||
brushes: HashMap::new(),
|
||||
}
|
||||
Self { hwnd: value }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +74,7 @@ impl Border {
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
}
|
||||
|
||||
pub fn create(id: &str, tracking_hwnd: isize) -> color_eyre::Result<Self> {
|
||||
pub fn create(id: &str) -> color_eyre::Result<Self> {
|
||||
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
|
||||
let class_name = PCWSTR(name.as_ptr());
|
||||
|
||||
@@ -158,6 +83,7 @@ impl Border {
|
||||
let window_class = WNDCLASSW {
|
||||
hInstance: h_module.into(),
|
||||
lpszClassName: class_name,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(Self::callback),
|
||||
hbrBackground: WindowsApi::create_solid_brush(0),
|
||||
..Default::default()
|
||||
@@ -165,36 +91,18 @@ impl Border {
|
||||
|
||||
let _ = WindowsApi::register_class_w(&window_class);
|
||||
|
||||
let (border_sender, border_receiver) = mpsc::channel();
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
|
||||
let instance = h_module.0 as isize;
|
||||
std::thread::spawn(move || -> color_eyre::Result<()> {
|
||||
let mut border = Self {
|
||||
hwnd: 0,
|
||||
render_target: OnceLock::new(),
|
||||
tracking_hwnd,
|
||||
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
style: STYLE.load(),
|
||||
width: BORDER_WIDTH.load(Ordering::Relaxed),
|
||||
offset: BORDER_OFFSET.load(Ordering::Relaxed),
|
||||
brush_properties: Default::default(),
|
||||
rounded_rect: Default::default(),
|
||||
brushes: HashMap::new(),
|
||||
};
|
||||
|
||||
let border_pointer = std::ptr::addr_of!(border);
|
||||
let hwnd =
|
||||
WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance, border_pointer)?;
|
||||
|
||||
border.hwnd = hwnd;
|
||||
border_sender.send(border_pointer as isize)?;
|
||||
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
|
||||
let mut msg: MSG = MSG::default();
|
||||
|
||||
loop {
|
||||
unsafe {
|
||||
if !GetMessageW(&mut msg, None, 0, 0).as_bool() {
|
||||
if !GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() {
|
||||
tracing::debug!("border window event processing thread shutdown");
|
||||
break;
|
||||
};
|
||||
@@ -202,123 +110,44 @@ impl Border {
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let border_ref = border_receiver.recv()?;
|
||||
let border = unsafe { &mut *(border_ref as *mut Border) };
|
||||
|
||||
// I have literally no idea, apparently this is to get rid of the black pixels
|
||||
// around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys
|
||||
unsafe {
|
||||
let pos: i32 = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8;
|
||||
let hrgn = CreateRectRgn(pos, 0, pos + 1, 1);
|
||||
let mut bh: DWM_BLURBEHIND = Default::default();
|
||||
if !hrgn.is_invalid() {
|
||||
bh = DWM_BLURBEHIND {
|
||||
dwFlags: DWM_BB_ENABLE | DWM_BB_BLURREGION,
|
||||
fEnable: TRUE,
|
||||
hRgnBlur: hrgn,
|
||||
fTransitionOnMaximized: FALSE,
|
||||
};
|
||||
}
|
||||
|
||||
let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh);
|
||||
}
|
||||
|
||||
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
|
||||
hwnd: HWND(windows_api::as_ptr!(border.hwnd)),
|
||||
pixelSize: Default::default(),
|
||||
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
|
||||
};
|
||||
|
||||
let render_target_properties = D2D1_RENDER_TARGET_PROPERTIES {
|
||||
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
||||
pixelFormat: D2D1_PIXEL_FORMAT {
|
||||
format: DXGI_FORMAT_UNKNOWN,
|
||||
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
|
||||
},
|
||||
dpiX: 96.0,
|
||||
dpiY: 96.0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match unsafe {
|
||||
RENDER_FACTORY
|
||||
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
|
||||
} {
|
||||
Ok(render_target) => unsafe {
|
||||
border.brush_properties = *BRUSH_PROPERTIES.deref();
|
||||
for window_kind in [
|
||||
WindowKind::Single,
|
||||
WindowKind::Stack,
|
||||
WindowKind::Monocle,
|
||||
WindowKind::Unfocused,
|
||||
WindowKind::Floating,
|
||||
] {
|
||||
let color = window_kind_colour(window_kind);
|
||||
let color = D2D1_COLOR_F {
|
||||
r: ((color & 0xFF) as f32) / 255.0,
|
||||
g: (((color >> 8) & 0xFF) as f32) / 255.0,
|
||||
b: (((color >> 16) & 0xFF) as f32) / 255.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
if let Ok(brush) =
|
||||
render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties))
|
||||
{
|
||||
border.brushes.insert(window_kind, brush);
|
||||
}
|
||||
}
|
||||
|
||||
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||
|
||||
if border
|
||||
.render_target
|
||||
.set(RenderTarget(render_target.clone()))
|
||||
.is_err()
|
||||
{
|
||||
return Err(anyhow!("could not store border render target"));
|
||||
}
|
||||
|
||||
border.rounded_rect = {
|
||||
let radius = 8.0 + border.width as f32 / 2.0;
|
||||
D2D1_ROUNDED_RECT {
|
||||
rect: Default::default(),
|
||||
radiusX: radius,
|
||||
radiusY: radius,
|
||||
}
|
||||
};
|
||||
|
||||
let mut render_targets = RENDER_TARGETS.lock();
|
||||
render_targets.insert(border.hwnd, RenderTarget(render_target));
|
||||
Ok(border.clone())
|
||||
},
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
Ok(Self {
|
||||
hwnd: hwnd_receiver.recv()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
let mut render_targets = RENDER_TARGETS.lock();
|
||||
render_targets.remove(&self.hwnd);
|
||||
WindowsApi::close_window(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> 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(self.width);
|
||||
rect.add_padding(-self.offset);
|
||||
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst));
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
|
||||
|
||||
WindowsApi::set_border_pos(self.hwnd, &rect, reference_hwnd)?;
|
||||
// Update the position of the border if required
|
||||
if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) {
|
||||
WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?;
|
||||
should_invalidate = true;
|
||||
}
|
||||
|
||||
// Invalidate the rect to trigger the callback to update colours etc.
|
||||
if should_invalidate {
|
||||
self.invalidate();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// this triggers WM_PAINT in the callback below
|
||||
pub fn invalidate(&self) {
|
||||
let _ = unsafe { InvalidateRect(Option::from(self.hwnd()), None, false) };
|
||||
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
|
||||
}
|
||||
|
||||
pub extern "system" fn callback(
|
||||
@@ -329,206 +158,75 @@ impl Border {
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_CREATE => {
|
||||
let mut border_pointer: *mut Border =
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
let create_struct: *mut CREATESTRUCTW = lparam.0 as *mut _;
|
||||
border_pointer = (*create_struct).lpCreateParams as *mut _;
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, border_pointer as _);
|
||||
}
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
EVENT_OBJECT_DESTROY => {
|
||||
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
// we don't actually want to destroy the window here, just hide it for quicker
|
||||
// visual feedback to the user; the actual destruction will be handled by the
|
||||
// core border manager loop
|
||||
WindowsApi::hide_window(window.0 as isize);
|
||||
LRESULT(0)
|
||||
}
|
||||
EVENT_OBJECT_LOCATIONCHANGE => {
|
||||
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let reference_hwnd = lparam.0;
|
||||
|
||||
let old_rect = (*border_pointer).window_rect;
|
||||
let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default();
|
||||
|
||||
(*border_pointer).window_rect = rect;
|
||||
|
||||
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if !rect.is_same_size_as(&old_rect) {
|
||||
if let Some(render_target) = (*border_pointer).render_target.get() {
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_PAINT => {
|
||||
if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
|
||||
let border_pointer: *mut Border =
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||
|
||||
// Update position to update the ZOrder
|
||||
let border_window_rect = (*border_pointer).window_rect;
|
||||
|
||||
tracing::trace!("updating border position");
|
||||
if let Err(error) =
|
||||
(*border_pointer).set_position(&border_window_rect, reference_hwnd)
|
||||
{
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if let Some(render_target) = (*border_pointer).render_target.get() {
|
||||
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
||||
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
||||
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
// 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)
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
// 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)),
|
||||
);
|
||||
|
||||
// Get window kind and color
|
||||
(*border_pointer).window_kind = FOCUS_STATE
|
||||
.lock()
|
||||
.get(&(window.0 as isize))
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused);
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
let hbrush = WindowsApi::create_solid_brush(0);
|
||||
|
||||
(*border_pointer).style = STYLE.load();
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
// 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 => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
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);
|
||||
}
|
||||
}
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hpen);
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hbrush);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("could not get border rect: {}", error.to_string())
|
||||
}
|
||||
}
|
||||
let _ = ValidateRect(Option::from(window), None);
|
||||
|
||||
// TODO: error handling
|
||||
let _ = EndPaint(window, &ps);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod border;
|
||||
use crate::Colour;
|
||||
use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::WindowKind;
|
||||
use crate::ring::Ring;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
pub use border::Border;
|
||||
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;
|
||||
@@ -22,24 +22,23 @@ use parking_lot::Mutex;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
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 strum::Display;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
|
||||
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
|
||||
|
||||
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
|
||||
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
|
||||
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
|
||||
AtomicCell::new(BorderImplementation::Komorebi);
|
||||
@@ -57,21 +56,7 @@ lazy_static! {
|
||||
lazy_static! {
|
||||
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
|
||||
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
|
||||
static ref WINDOWS_BORDERS: Mutex<HashMap<isize, Border>> = Mutex::new(HashMap::new());
|
||||
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
|
||||
static ref RENDER_TARGETS: Mutex<HashMap<isize, RenderTarget>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderTarget(pub ID2D1HwndRenderTarget);
|
||||
unsafe impl Send for RenderTarget {}
|
||||
|
||||
impl Deref for RenderTarget {
|
||||
type Target = ID2D1HwndRenderTarget;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Notification(pub Option<isize>);
|
||||
@@ -90,10 +75,6 @@ fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn window_border(hwnd: isize) -> Option<Border> {
|
||||
WINDOWS_BORDERS.lock().get(&hwnd).cloned()
|
||||
}
|
||||
|
||||
pub fn send_notification(hwnd: Option<isize>) {
|
||||
if event_tx().try_send(Notification(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
@@ -108,14 +89,12 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
);
|
||||
|
||||
for (_, border) in borders.iter() {
|
||||
let _ = border.destroy();
|
||||
border.destroy()?;
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
BORDERS_MONITORS.lock().clear();
|
||||
WINDOWS_BORDERS.lock().clear();
|
||||
FOCUS_STATE.lock().clear();
|
||||
RENDER_TARGETS.lock().clear();
|
||||
|
||||
let mut remaining_hwnds = vec![];
|
||||
|
||||
@@ -128,7 +107,7 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
tracing::info!("purging unknown borders: {:?}", remaining_hwnds);
|
||||
|
||||
for hwnd in remaining_hwnds {
|
||||
let _ = Border::from(hwnd).destroy();
|
||||
Border::from(hwnd).destroy()?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,24 +116,22 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
|
||||
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
||||
match focus_kind {
|
||||
WindowKind::Unfocused => UNFOCUSED.load(Ordering::Relaxed),
|
||||
WindowKind::Single => FOCUSED.load(Ordering::Relaxed),
|
||||
WindowKind::Stack => STACK.load(Ordering::Relaxed),
|
||||
WindowKind::Monocle => MONOCLE.load(Ordering::Relaxed),
|
||||
WindowKind::Floating => FLOATING.load(Ordering::Relaxed),
|
||||
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()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -163,6 +140,7 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
let receiver = event_rx();
|
||||
event_tx().send(Notification(None))?;
|
||||
|
||||
@@ -170,7 +148,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let mut previous_pending_move_op = None;
|
||||
let mut previous_is_paused = false;
|
||||
let mut previous_notification: Option<Notification> = None;
|
||||
let mut previous_layer = WorkspaceLayer::default();
|
||||
|
||||
'receiver: for notification in receiver {
|
||||
// Check the wm state every time we receive a notification
|
||||
@@ -180,17 +157,13 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let focused_workspace_idx =
|
||||
state.monitors.elements()[focused_monitor_idx].focused_workspace_idx();
|
||||
let monitors = state.monitors.clone();
|
||||
let pending_move_op = *state.pending_move_op;
|
||||
let pending_move_op = state.pending_move_op;
|
||||
let floating_window_hwnds = state.monitors.elements()[focused_monitor_idx].workspaces()
|
||||
[focused_workspace_idx]
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
let workspace_layer = *state.monitors.elements()[focused_monitor_idx].workspaces()
|
||||
[focused_workspace_idx]
|
||||
.layer();
|
||||
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
|
||||
|
||||
drop(state);
|
||||
|
||||
@@ -232,16 +205,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
}
|
||||
|
||||
for window in ws.floating_windows() {
|
||||
let mut window_kind = WindowKind::Unfocused;
|
||||
|
||||
if foreground_window == window.hwnd {
|
||||
window_kind = WindowKind::Floating;
|
||||
}
|
||||
|
||||
window.set_accent(window_kind_colour(window_kind))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,20 +233,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// when we switch focus to/from a floating window
|
||||
let switch_focus_to_from_floating_window = floating_window_hwnds.iter().any(|fw| {
|
||||
// if we switch focus to a floating window
|
||||
fw == ¬ification.0.unwrap_or_default() ||
|
||||
// if there is any floating window with a `WindowKind::Floating` border
|
||||
// that no longer is the foreground window then we need to update that
|
||||
// border.
|
||||
(fw != &foreground_window
|
||||
&& window_border(*fw)
|
||||
.map(|b| b.window_kind == WindowKind::Floating)
|
||||
.unwrap_or_default())
|
||||
});
|
||||
|
||||
if !should_process_notification && switch_focus_to_from_floating_window {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -302,11 +255,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
let mut borders_monitors = BORDERS_MONITORS.lock();
|
||||
let mut windows_borders = WINDOWS_BORDERS.lock();
|
||||
let mut focus_state = FOCUS_STATE.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
|
||||
@@ -318,9 +271,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
borders_monitors.clear();
|
||||
windows_borders.clear();
|
||||
focus_state.clear();
|
||||
|
||||
previous_is_paused = is_paused;
|
||||
continue 'receiver;
|
||||
@@ -331,100 +281,98 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Workspaces with tiling disabled don't have borders
|
||||
if !ws.tile() {
|
||||
// Remove all borders on this monitor
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
&mut focus_state,
|
||||
&mut borders_monitors,
|
||||
monitor_idx,
|
||||
|_, _| true,
|
||||
)?;
|
||||
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 mut new_border = false;
|
||||
let border = match borders.entry(monocle.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
match Border::create(
|
||||
monocle.id(),
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd,
|
||||
) {
|
||||
Ok(border) => {
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
}
|
||||
_ => {
|
||||
continue 'monitors;
|
||||
}
|
||||
if let Ok(border) = Border::create(monocle.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_focus_state = if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
};
|
||||
border.window_kind = new_focus_state;
|
||||
focus_state.insert(border.hwnd, new_focus_state);
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
|
||||
let reference_hwnd =
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd;
|
||||
|
||||
let rect = WindowsApi::window_rect(reference_hwnd)?;
|
||||
|
||||
if new_border {
|
||||
border.set_position(&rect, reference_hwnd)?;
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
focus_state.insert(
|
||||
border.hwnd,
|
||||
if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
WindowKind::Monocle
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
border.invalidate();
|
||||
let rect = WindowsApi::window_rect(
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
windows_borders.insert(
|
||||
monocle.focused_window().cloned().unwrap_or_default().hwnd,
|
||||
border.clone(),
|
||||
);
|
||||
border.update(&rect, true)?;
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
// Remove all borders on this monitor except monocle
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
&mut focus_state,
|
||||
&mut borders_monitors,
|
||||
monitor_idx,
|
||||
|_, b| border_hwnd != b.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 foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let foreground_monitor_id =
|
||||
WindowsApi::monitor_from_window(foreground_hwnd);
|
||||
let is_maximized = foreground_monitor_id == m.id()
|
||||
&& WindowsApi::is_zoomed(foreground_hwnd);
|
||||
let is_maximized = WindowsApi::is_zoomed(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
);
|
||||
|
||||
if is_maximized {
|
||||
// Remove all borders on this monitor
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
&mut focus_state,
|
||||
&mut borders_monitors,
|
||||
monitor_idx,
|
||||
|_, _| true,
|
||||
)?;
|
||||
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;
|
||||
}
|
||||
|
||||
// Collect focused workspace container and floating windows ID's
|
||||
// Destroy any borders not associated with the focused workspace
|
||||
let mut container_and_floating_window_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
@@ -435,77 +383,82 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
container_and_floating_window_ids.push(w.hwnd.to_string());
|
||||
}
|
||||
|
||||
// Remove any borders not associated with the focused workspace
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
&mut focus_state,
|
||||
&mut borders_monitors,
|
||||
monitor_idx,
|
||||
|id, _| !container_and_floating_window_ids.contains(id),
|
||||
)?;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
'containers: for (idx, c) in ws.containers().iter().enumerate() {
|
||||
// In case this container is a stack we need to check it's
|
||||
// unfocused windows to remove any attached border
|
||||
let is_stack = c.windows().len() > 1;
|
||||
if is_stack {
|
||||
let focused_window_idx = c.focused_window_idx();
|
||||
let potential_stacked_border_handles = c
|
||||
.windows()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(i, w)| {
|
||||
if i != focused_window_idx {
|
||||
windows_borders.get(&w.hwnd).map(|b| b.hwnd)
|
||||
} else {
|
||||
None
|
||||
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;
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
};
|
||||
|
||||
if !potential_stacked_border_handles.is_empty() {
|
||||
tracing::debug!(
|
||||
"purging stacked borders: {:?}",
|
||||
potential_stacked_border_handles
|
||||
);
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
&mut focus_state,
|
||||
&mut borders_monitors,
|
||||
monitor_idx,
|
||||
|_, b| potential_stacked_border_handles.contains(&b.hwnd),
|
||||
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;
|
||||
}
|
||||
|
||||
let focused_window_hwnd =
|
||||
c.focused_window().map(|w| w.hwnd).unwrap_or_default();
|
||||
|
||||
// Get the border entry for this container from the map or create one
|
||||
let mut new_border = false;
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
match Border::create(c.id(), focused_window_hwnd) {
|
||||
Ok(border) => {
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
}
|
||||
_ => {
|
||||
continue 'monitors;
|
||||
}
|
||||
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
|
||||
|| focused_window_hwnd != foreground_window
|
||||
{
|
||||
WindowKind::Unfocused
|
||||
} else if c.windows().len() > 1 {
|
||||
@@ -513,141 +466,118 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
} else {
|
||||
WindowKind::Single
|
||||
};
|
||||
border.window_kind = new_focus_state;
|
||||
|
||||
last_focus_state = focus_state.get(&border.hwnd).copied();
|
||||
|
||||
// If this container's border was previously tracking a different
|
||||
// window, then we need to destroy that border and create a new one
|
||||
// tracking the correct window.
|
||||
if border.tracking_hwnd != focused_window_hwnd {
|
||||
// Create new border
|
||||
match Border::create(
|
||||
c.id(),
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
) {
|
||||
Ok(b) => {
|
||||
// Destroy previously stacked border window and remove its hwnd
|
||||
// and tracking_hwnd.
|
||||
border.destroy()?;
|
||||
focus_state.remove(&border.hwnd);
|
||||
if let Some(previous) =
|
||||
windows_borders.get(&border.tracking_hwnd)
|
||||
{
|
||||
// Only remove the border from `windows_borders` if it
|
||||
// still is the same border, if it isn't then it means it
|
||||
// was already updated by another border for that window
|
||||
// and in that case we don't want to remove it.
|
||||
if previous.hwnd == border.hwnd {
|
||||
windows_borders.remove(&border.tracking_hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace with new border
|
||||
new_border = true;
|
||||
*border = b;
|
||||
}
|
||||
_ => {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
// avoid getting into a thread restart loop if we try to look up
|
||||
// rect info for a window that has been destroyed by the time
|
||||
// we get here
|
||||
let rect = match WindowsApi::window_rect(focused_window_hwnd) {
|
||||
Ok(rect) => rect,
|
||||
Err(_) => {
|
||||
remove_border(
|
||||
c.id(),
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
&mut focus_state,
|
||||
&mut borders_monitors,
|
||||
)?;
|
||||
continue 'containers;
|
||||
}
|
||||
};
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
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) || layer_changed
|
||||
}
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
if new_border || should_invalidate {
|
||||
border.set_position(&rect, focused_window_hwnd)?;
|
||||
}
|
||||
|
||||
if should_invalidate {
|
||||
border.invalidate();
|
||||
}
|
||||
|
||||
borders_monitors.insert(c.id().clone(), monitor_idx);
|
||||
windows_borders.insert(
|
||||
c.focused_window().cloned().unwrap_or_default().hwnd,
|
||||
border.clone(),
|
||||
);
|
||||
focus_state.insert(border.hwnd, new_focus_state);
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
|
||||
{
|
||||
for window in ws.floating_windows() {
|
||||
let mut new_border = false;
|
||||
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) => {
|
||||
match Border::create(&window.hwnd.to_string(), window.hwnd)
|
||||
if let Ok(border) = Border::create(&window.hwnd.to_string())
|
||||
{
|
||||
Ok(border) => {
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
}
|
||||
_ => {
|
||||
continue 'monitors;
|
||||
}
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
let mut new_focus_state = WindowKind::Unfocused;
|
||||
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
|
||||
|
||||
if foreground_window == window.hwnd {
|
||||
new_focus_state = WindowKind::Floating;
|
||||
let mut should_destroy = false;
|
||||
|
||||
if let Some(notification_hwnd) = notification.0 {
|
||||
if notification_hwnd != window.hwnd {
|
||||
should_destroy = true;
|
||||
}
|
||||
}
|
||||
|
||||
border.window_kind = new_focus_state;
|
||||
last_focus_state = focus_state.get(&border.hwnd).copied();
|
||||
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 layer_changed = previous_layer != workspace_layer;
|
||||
|
||||
let should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
Some(last_focus_state) => {
|
||||
last_focus_state != new_focus_state || layer_changed
|
||||
}
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
if new_border {
|
||||
border.set_position(&rect, window.hwnd)?;
|
||||
}
|
||||
|
||||
if should_invalidate {
|
||||
border.invalidate();
|
||||
}
|
||||
|
||||
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
|
||||
windows_borders.insert(window.hwnd, border.clone());
|
||||
focus_state.insert(border.hwnd, new_focus_state);
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -658,59 +588,12 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
previous_pending_move_op = pending_move_op;
|
||||
previous_is_paused = is_paused;
|
||||
previous_notification = Some(notification);
|
||||
previous_layer = workspace_layer;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes all borders from monitor with index `monitor_idx` filtered by
|
||||
/// `condition`. This condition is a function that will take a reference to
|
||||
/// the container id and the border and returns a bool, if true that border
|
||||
/// will be removed.
|
||||
fn remove_borders(
|
||||
borders: &mut HashMap<String, Border>,
|
||||
windows_borders: &mut HashMap<isize, Border>,
|
||||
focus_state: &mut HashMap<isize, WindowKind>,
|
||||
borders_monitors: &mut HashMap<String, usize>,
|
||||
monitor_idx: usize,
|
||||
condition: impl Fn(&String, &Border) -> bool,
|
||||
) -> color_eyre::Result<()> {
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
|
||||
&& condition(id, border)
|
||||
{
|
||||
to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for id in &to_remove {
|
||||
remove_border(id, borders, windows_borders, focus_state, borders_monitors)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the border with `id` and all its related info from all maps
|
||||
fn remove_border(
|
||||
id: &str,
|
||||
borders: &mut HashMap<String, Border>,
|
||||
windows_borders: &mut HashMap<isize, Border>,
|
||||
focus_state: &mut HashMap<isize, WindowKind>,
|
||||
borders_monitors: &mut HashMap<String, usize>,
|
||||
) -> color_eyre::Result<()> {
|
||||
if let Some(removed_border) = borders.remove(id) {
|
||||
removed_border.destroy()?;
|
||||
windows_borders.remove(&removed_border.tracking_hwnd);
|
||||
focus_state.remove(&removed_border.hwnd);
|
||||
}
|
||||
borders_monitors.remove(id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum ZOrder {
|
||||
Top,
|
||||
NoTopMost,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use hex_color::HexColor;
|
||||
use komorebi_themes::Color32;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaGenerator;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::InstanceType;
|
||||
use schemars::schema::Schema;
|
||||
use schemars::schema::SchemaObject;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum Colour {
|
||||
/// Colour represented as RGB
|
||||
@@ -39,19 +39,7 @@ impl From<Color32> for Colour {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Colour> for Color32 {
|
||||
fn from(value: Colour) -> Self {
|
||||
match value {
|
||||
Colour::Rgb(rgb) => Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8),
|
||||
Colour::Hex(hex) => {
|
||||
let rgb = Rgb::from(hex);
|
||||
Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct Hex(HexColor);
|
||||
|
||||
impl JsonSchema for Hex {
|
||||
@@ -78,7 +66,7 @@ impl From<Colour> for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Rgb {
|
||||
/// Red
|
||||
pub r: u32,
|
||||
@@ -120,8 +108,8 @@ impl From<u32> for Rgb {
|
||||
fn from(value: u32) -> Self {
|
||||
Self {
|
||||
r: value & 0xff,
|
||||
g: (value >> 8) & 0xff,
|
||||
b: (value >> 16) & 0xff,
|
||||
g: value >> 8 & 0xff,
|
||||
b: value >> 16 & 0xff,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::ops::Deref;
|
||||
use windows::core::IUnknown;
|
||||
use windows::core::IUnknown_Vtbl;
|
||||
use windows::core::GUID;
|
||||
use windows::core::HRESULT;
|
||||
use windows::core::HSTRING;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::core::PWSTR;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::RECT;
|
||||
use windows::Win32::Foundation::SIZE;
|
||||
use windows::Win32::UI::Shell::Common::IObjectArray;
|
||||
use windows::core::GUID;
|
||||
use windows::core::HRESULT;
|
||||
use windows::core::HSTRING;
|
||||
use windows::core::IUnknown;
|
||||
use windows::core::IUnknown_Vtbl;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::core::PWSTR;
|
||||
use windows_core::BOOL;
|
||||
|
||||
type DesktopID = GUID;
|
||||
|
||||
@@ -44,7 +44,7 @@ impl<'a, T: Clone> ComIn<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for ComIn<'_, T> {
|
||||
impl<'a, T> Deref for ComIn<'a, T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
@@ -129,7 +129,7 @@ pub unsafe trait IApplicationView: IUnknown {
|
||||
pub unsafe fn get_app_user_model_id(&self, id: *mut PWSTR) -> HRESULT; // Proc17
|
||||
pub unsafe fn set_app_user_model_id(&self, id: PCWSTR) -> HRESULT;
|
||||
pub unsafe fn is_equal_by_app_user_model_id(&self, id: PCWSTR, out_result: *mut INT)
|
||||
-> HRESULT;
|
||||
-> HRESULT;
|
||||
|
||||
/*** IApplicationView methods ***/
|
||||
pub unsafe fn get_view_state(&self, out_state: *mut UINT) -> HRESULT; // Proc20
|
||||
|
||||
@@ -11,11 +11,11 @@ use interfaces::IServiceProvider;
|
||||
use std::ffi::c_void;
|
||||
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||
use windows::Win32::System::Com::COINIT_MULTITHREADED;
|
||||
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();
|
||||
@@ -23,7 +23,10 @@ struct ComInit();
|
||||
impl ComInit {
|
||||
pub fn new() -> Self {
|
||||
unsafe {
|
||||
CoInitializeEx(None, COINIT_MULTITHREADED).unwrap();
|
||||
// Notice: Only COINIT_APARTMENTTHREADED works correctly!
|
||||
//
|
||||
// Not COINIT_MULTITHREADED or CoIncrementMTAUsage, they cause a seldom crashes in threading tests.
|
||||
CoInitializeEx(None, COINIT_APARTMENTTHREADED).unwrap();
|
||||
}
|
||||
Self()
|
||||
}
|
||||
@@ -64,7 +67,7 @@ fn get_iapplication_view_collection(provider: &IServiceProvider) -> IApplication
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
|
||||
COM_INIT.with(|_| {
|
||||
let provider = get_iservice_provider();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user