mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-14 14:23:36 +01:00
Compare commits
114 Commits
feature/ba
...
feature/fl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1897e1845f | ||
|
|
192af6751b | ||
|
|
4f306e5bfd | ||
|
|
ede0b23bb4 | ||
|
|
e6b5b78857 | ||
|
|
440d78e8f4 | ||
|
|
280ca0ffcd | ||
|
|
f227bd0fef | ||
|
|
3781c8ea41 | ||
|
|
33800903f7 | ||
|
|
bb31e7155d | ||
|
|
40b32332ae | ||
|
|
8743cdd292 | ||
|
|
01367f59e2 | ||
|
|
e6ddccc5f6 | ||
|
|
b22ec90438 | ||
|
|
46b81e4372 | ||
|
|
6f00c527a4 | ||
|
|
3ad4090df8 | ||
|
|
449ccac645 | ||
|
|
1d00196240 | ||
|
|
639ebd0b3d | ||
|
|
e22eafbc8e | ||
|
|
46c2ad512b | ||
|
|
3de96609bb | ||
|
|
9c09284b0f | ||
|
|
b14c0d07a2 | ||
|
|
3615451f41 | ||
|
|
6893f39dd9 | ||
|
|
041ef5731c | ||
|
|
779c12bc6a | ||
|
|
0e48370b73 | ||
|
|
8de92ec32a | ||
|
|
ac243c6992 | ||
|
|
d001d8a7a6 | ||
|
|
219fa8e14f | ||
|
|
e4e94fd1a6 | ||
|
|
e707a14b8a | ||
|
|
818ec1c63b | ||
|
|
a10bb467e5 | ||
|
|
4fd60bbff3 | ||
|
|
cc196db046 | ||
|
|
7f0b54c35e | ||
|
|
b1726af2eb | ||
|
|
fd8cd4bb01 | ||
|
|
172988ed81 | ||
|
|
36e3eaad36 | ||
|
|
0f022d47df | ||
|
|
dc1eb8ff50 | ||
|
|
166f505aba | ||
|
|
d55d356b37 | ||
|
|
9a3dbccc89 | ||
|
|
24b43a154c | ||
|
|
6a09ec4b87 | ||
|
|
374cd2c6d5 | ||
|
|
0f385d6245 | ||
|
|
5503323695 | ||
|
|
aa9f50fd5c | ||
|
|
3a6ae01b12 | ||
|
|
720089587b | ||
|
|
852d1f9eb0 | ||
|
|
ed5b0f9120 | ||
|
|
7f7b8c7c05 | ||
|
|
4198f6fabc | ||
|
|
4acd408b88 | ||
|
|
73d928771d | ||
|
|
d194afe6e6 | ||
|
|
72f4ed0575 | ||
|
|
82ff69e337 | ||
|
|
583d8b1e4c | ||
|
|
2f1f1b160c | ||
|
|
bcefea8a1a | ||
|
|
223404a1aa | ||
|
|
7d032db507 | ||
|
|
09afad624f | ||
|
|
903b6af507 | ||
|
|
1644fcf6f2 | ||
|
|
b31e8911ce | ||
|
|
298fbafe00 | ||
|
|
e46a1a6117 | ||
|
|
34929f32a7 | ||
|
|
1ef7a09163 | ||
|
|
c0c3c81d69 | ||
|
|
7276dc2309 | ||
|
|
58d3086615 | ||
|
|
aa5a36989f | ||
|
|
b612066367 | ||
|
|
cbe5b24f73 | ||
|
|
2ead216eeb | ||
|
|
b0944662fa | ||
|
|
1376d7be04 | ||
|
|
5da72e10df | ||
|
|
d21ffb28cc | ||
|
|
4e27febdc0 | ||
|
|
3c5852ae20 | ||
|
|
7943fccb1b | ||
|
|
2d2cea31c0 | ||
|
|
a17924c2af | ||
|
|
91519227d4 | ||
|
|
f91d0aabf5 | ||
|
|
d5b6584042 | ||
|
|
6db317d425 | ||
|
|
07a1538905 | ||
|
|
853db2f15f | ||
|
|
c9f180ce0f | ||
|
|
bc00f54c90 | ||
|
|
c57759242a | ||
|
|
6f27de8e5c | ||
|
|
a9e98034b0 | ||
|
|
3489163793 | ||
|
|
71d65cf4a1 | ||
|
|
a5e6828d1e | ||
|
|
96605f72a7 | ||
|
|
18bb060b71 |
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,52 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]: Short descriptive title"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See bug
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots and Videos**
|
||||
Add screenshots and videos to help explain your problem.
|
||||
|
||||
**Operating System**
|
||||
Provide the output of `systeminfo | grep "^OS Name\|^OS Version"`
|
||||
|
||||
For example:
|
||||
```
|
||||
OS Name: Microsoft Windows 11 Pro
|
||||
OS Version: 10.0.22000 N/A Build 22000
|
||||
```
|
||||
|
||||
**`komorebic check` Output**
|
||||
Provide the output of `komorebic check`
|
||||
|
||||
For example:
|
||||
```
|
||||
No KOMOREBI_CONFIG_HOME detected, defaulting to C:\Users\LGUG2Z
|
||||
|
||||
Looking for configuration files in C:\Users\LGUG2Z
|
||||
|
||||
No komorebi configuration found in C:\Users\LGUG2Z
|
||||
|
||||
If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration
|
||||
```
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
In particular, if you have any other AHK scripts or software running that handle any aspect of window management or manipulation
|
||||
64
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
64
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
labels: [ bug ]
|
||||
title: "[BUG]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please **do not** open an issue for applications with invisible windows leaving ghost tiles.
|
||||
|
||||
You can run `komorebic visible-windows` when the ghost tile is present on your workspace to retrieve the invisible window's exe, class name and title, and then use that to [ignore the window](https://lgug2z.github.io/komorebi/common-workflows/ignore-windows.html) responsible for the ghost tile.
|
||||
|
||||
If it is not possible to uniquely identify the invisible window resulting in a ghost tile through a mixture of exe, title and class identifiers , then this is not a bug with komorebi but a bug with the application you are using, and should open an issue with the developer(s) of that application.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
description: >
|
||||
Please provide a short summary of the bug, along with any information
|
||||
you feel is relevant to replicating the bug.
|
||||
|
||||
You may include screenshots and videos in this section.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Version Information
|
||||
description: >
|
||||
Please provide information about the versions of Windows and komorebi
|
||||
running on your machine.
|
||||
|
||||
Do not submit a bug if you are not using an official version of Windows
|
||||
such as AtlasOS; only official versions of Windows are supported.
|
||||
|
||||
```
|
||||
systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version"
|
||||
```
|
||||
|
||||
```
|
||||
komorebic --version
|
||||
```
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Komorebi Configuration
|
||||
description: >
|
||||
Please provide your configuration file (komorebi.json or komorebi.bar.json)
|
||||
render: json
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Hotkey Configuration
|
||||
description: >
|
||||
Please provide your whkdrc or komorebi.ahk hotkey configuration file
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Output of komorebic check
|
||||
description: >
|
||||
Please provide the output of `komorebic check`
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Komorebi Documentation
|
||||
url: https://lgug2z.github.io/komorebi/
|
||||
about: Please search the documentation website before opening an issue
|
||||
- name: Komorebi Quickstart Tutorial Video
|
||||
url: https://www.youtube.com/watch?v=MMZUAtHbTYY
|
||||
about: If you are new, please make sure you watch the quickstart tutorial video
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEAT]: Short descriptive title"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
37
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
37
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Feature request
|
||||
description: Suggest a new feature (Sponsors only)
|
||||
labels: [enhancement]
|
||||
title: "[FEAT]: "
|
||||
body:
|
||||
- type: dropdown
|
||||
id: Sponsors
|
||||
attributes:
|
||||
label: Sponsorship Information
|
||||
description: >
|
||||
Feature requests are considered from individuals who are $5+ monthly sponsors to the project.
|
||||
|
||||
Please specify the platform you use to sponsor the project.
|
||||
options:
|
||||
- GitHub Sponsors
|
||||
- Ko-fi
|
||||
- Discord
|
||||
- YouTube
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Suggestion
|
||||
description: >
|
||||
Please share your suggestion here. Be sure to include all necessary context.
|
||||
|
||||
If you sponsor on a platform where you use a different username, please specify the username here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: >
|
||||
Please share share alternatives you have considered here.
|
||||
7
.github/pull_request_template.md
vendored
Normal file
7
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<!--
|
||||
Please follow the Conventional Commits specification.
|
||||
|
||||
If you need to update your PR with changes from `master`, please run `git rebase master`.
|
||||
|
||||
By opening this PR, you confirm that you have read and understood this project's `CONTRIBUTING.md`.
|
||||
-->
|
||||
125
.github/workflows/sponsor-check.yaml
vendored
Normal file
125
.github/workflows/sponsor-check.yaml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
name: Feature Request Sponsor Check
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_username:
|
||||
description: "Test username to check sponsorship for"
|
||||
required: true
|
||||
default: "octocat"
|
||||
test_title:
|
||||
description: "Test issue title"
|
||||
required: true
|
||||
default: "[FEAT] Test Feature Request"
|
||||
test_sponsor_platform:
|
||||
description: "Selected sponsor platform"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- "GitHub Sponsors"
|
||||
- "Ko-fi"
|
||||
- "Discord"
|
||||
- "YouTube"
|
||||
|
||||
jobs:
|
||||
check-sponsor:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'workflow_dispatch') || (github.event_name == 'issues' &&
|
||||
startsWith(github.event.issue.title, '[FEAT]') &&
|
||||
github.event.issue.user.login != 'LGUG2Z' &&
|
||||
fromJSON(github.event.issue.body).Sponsors == 'GitHub Sponsors')
|
||||
|
||||
steps:
|
||||
- name: Get Issue Details
|
||||
id: issue-details
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "username=${{ github.event.inputs.test_username }}" >> $GITHUB_OUTPUT
|
||||
echo "title=${{ github.event.inputs.test_title }}" >> $GITHUB_OUTPUT
|
||||
echo "sponsor_platform=${{ github.event.inputs.test_sponsor_platform }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "username=${{ github.event.issue.user.login }}" >> $GITHUB_OUTPUT
|
||||
echo "title=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT
|
||||
echo "sponsor_platform=$(jq -r '.Sponsors' <<< '${{ github.event.issue.body }}')" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Get Sponsorship Status
|
||||
id: sponsorship
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.PAT }}
|
||||
script: |
|
||||
const username = '${{ steps.issue-details.outputs.username }}';
|
||||
const sponsorPlatform = '${{ steps.issue-details.outputs.sponsor_platform }}';
|
||||
|
||||
if (sponsorPlatform !== 'GitHub Sponsors') {
|
||||
console.log('Sponsor platform is not GitHub Sponsors, skipping check');
|
||||
return true;
|
||||
}
|
||||
|
||||
const sponsorshipQuery = `query($user: String!) {
|
||||
user(login: $user) {
|
||||
... on Sponsorable {
|
||||
sponsorshipForViewerAsSponsorable {
|
||||
tier {
|
||||
name
|
||||
monthlyPriceInDollars
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
try {
|
||||
const result = await github.graphql(sponsorshipQuery, {
|
||||
user: username
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
const sponsorship = result.user.sponsorshipForViewerAsSponsorable;
|
||||
console.log(sponsorship);
|
||||
const amount = sponsorship?.tier?.monthlyPriceInDollars || 0;
|
||||
|
||||
console.log(`Sponsorship amount for ${username}: $${amount}/month`);
|
||||
return amount >= 5;
|
||||
} catch (error) {
|
||||
console.log(`Error checking sponsorship: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
- name: Print Test Results
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Test Results for ${{ steps.issue-details.outputs.username }}:"
|
||||
echo "Title: ${{ steps.issue-details.outputs.title }}"
|
||||
echo "Platform: ${{ steps.issue-details.outputs.sponsor_platform }}"
|
||||
echo "Would close issue: ${{ steps.sponsorship.outputs.result == 'false' }}"
|
||||
|
||||
- name: Close Issue If Not Sponsor
|
||||
if: |
|
||||
github.event_name == 'issues' &&
|
||||
steps.sponsorship.outputs.result == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issueNumber = context.issue.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body: 'Thank you for your feature request! This repository requires a GitHub sponsorship of at least $5/month to submit feature requests. Please consider becoming a sponsor at https://github.com/sponsors/LGUG2Z'
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed'
|
||||
});
|
||||
254
.github/workflows/windows.yaml
vendored
254
.github/workflows/windows.yaml
vendored
@@ -15,109 +15,88 @@ on:
|
||||
- v*
|
||||
schedule:
|
||||
- cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: windows-latest
|
||||
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
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- x86_64-pc-windows-msvc
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Prep cargo dirs
|
||||
run: |
|
||||
New-Item "${env:USERPROFILE}\.cargo\registry" -ItemType Directory -Force
|
||||
New-Item "${env:USERPROFILE}\.cargo\git" -ItemType Directory -Force
|
||||
shell: powershell
|
||||
- name: Set environment variables appropriately for the build
|
||||
run: |
|
||||
echo "%USERPROFILE%\.cargo\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8
|
||||
echo "TARGET=${{ matrix.target }}" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
||||
echo "SKIP_TESTS=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
|
||||
- name: Cache cargo registry, git trees and binaries
|
||||
uses: actions/cache@v4
|
||||
- run: rustup toolchain install stable --profile minimal
|
||||
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Get rustc commit hash
|
||||
id: cargo-target-cache
|
||||
run: |
|
||||
echo "::set-output name=rust_hash::$(rustc -Vv | grep commit-hash | awk '{print $2}')"
|
||||
shell: bash
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v4
|
||||
cache-on-failure: "true"
|
||||
cache-all-crates: "true"
|
||||
key: ${{ matrix.platform.target }}
|
||||
- run: cargo +nightly fmt --check
|
||||
- run: cargo clippy
|
||||
- uses: houseabsolute/actions-rust-cross@v0
|
||||
with:
|
||||
path: target
|
||||
key: ${{ github.base_ref }}-${{ github.head_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ github.base_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Install Rustup using win.rustup.rs
|
||||
run: |
|
||||
# Disable the download progress bar which can cause perf issues
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
|
||||
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --profile=minimal
|
||||
shell: powershell
|
||||
- name: Ensure stable toolchain is up to date
|
||||
run: rustup update stable
|
||||
shell: bash
|
||||
- name: Install the target
|
||||
run: |
|
||||
rustup target install ${{ matrix.target }}
|
||||
- name: Run Cargo checks
|
||||
run: |
|
||||
cargo fmt --check
|
||||
cargo check
|
||||
cargo clippy
|
||||
- name: Run a full build
|
||||
run: |
|
||||
cargo build --locked --release --target ${{ matrix.target }}
|
||||
- name: Create MSI installer
|
||||
run: |
|
||||
command: "build"
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: "--locked --release"
|
||||
- run: |
|
||||
cargo install cargo-wix
|
||||
cargo wix -p komorebi --nocapture -I .\wix\main.wxs --target x86_64-pc-windows-msvc
|
||||
- name: Upload the built artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: komorebi-${{ matrix.target }}
|
||||
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
|
||||
path: |
|
||||
target/${{ matrix.target }}/release/komorebi.exe
|
||||
target/${{ matrix.target }}/release/komorebic.exe
|
||||
target/${{ matrix.target }}/release/komorebic-no-console.exe
|
||||
target/${{ matrix.target }}/release/komorebi-bar.exe
|
||||
target/${{ matrix.target }}/release/komorebi-gui.exe
|
||||
target/${{ matrix.target }}/release/komorebi.pdb
|
||||
target/${{ matrix.target }}/release/komorebic.pdb
|
||||
target/${{ matrix.target }}/release/komorebi_gui.pdb
|
||||
target/${{ matrix.platform.target }}/release/*.exe
|
||||
target/${{ matrix.platform.target }}/release/*.pdb
|
||||
target/wix/komorebi-*.msi
|
||||
retention-days: 7
|
||||
- name: Check GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
env:
|
||||
GORELEASER_CURRENT_TAG: v0.1.29
|
||||
retention-days: 14
|
||||
|
||||
nightly:
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
permissions: write-all
|
||||
if: ${{ github.ref == 'refs/heads/master' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' ) }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
version: latest
|
||||
args: build --skip=validate --clean
|
||||
- name: Prepare nightly artifacts
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
|
||||
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
|
||||
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
|
||||
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
|
||||
git tag -d nightly || true
|
||||
git tag nightly
|
||||
kokai release --no-emoji --add-links github:commits,issues --ref nightly >"CHANGELOG.md"
|
||||
- shell: bash
|
||||
run: |
|
||||
gh release delete nightly --yes || true
|
||||
git push origin :nightly || true
|
||||
@@ -125,39 +104,100 @@ jobs:
|
||||
--target $GITHUB_SHA \
|
||||
--prerelease \
|
||||
--title "komorebi nightly (${GITHUB_SHA})" \
|
||||
--notes "This nightly release of komorebi corresponds to [this commit](https://github.com/LGUG2Z/komorebi/commit/${GITHUB_SHA})." \
|
||||
--notes-file CHANGELOG.md \
|
||||
komorebi-nightly-x86_64-pc-windows-msvc.zip \
|
||||
komorebi-nightly-x86_64.msi \
|
||||
komorebi-nightly-aarch64-pc-windows-msvc.zip \
|
||||
komorebi-nightly-aarch64.msi \
|
||||
checksums.txt
|
||||
# Release
|
||||
- name: Generate changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
|
||||
release-dry-run:
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
permissions: write-all
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
|
||||
|
||||
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
|
||||
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-on-failure: "true"
|
||||
cache-all-crates: "true"
|
||||
- shell: bash
|
||||
run: |
|
||||
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
|
||||
git tag -d nightly
|
||||
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
|
||||
kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md"
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --skip=validate --clean --release-notes=CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
|
||||
- name: Add MSI to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
files: "target/wix/komorebi-*.msi"
|
||||
body_path: "CHANGELOG.md"
|
||||
files: |
|
||||
checksums.txt
|
||||
*.zip
|
||||
*.msi
|
||||
|
||||
winget:
|
||||
name: Publish to WinGet
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
needs: release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,4 +4,5 @@
|
||||
CHANGELOG.md
|
||||
dummy.go
|
||||
komorebic/applications.yaml
|
||||
komorebic/applications.json
|
||||
/.vs
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# Adapted from https://jondot.medium.com/shipping-rust-binaries-with-goreleaser-d5aa42a46be0
|
||||
project_name: komorebi
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- powershell.exe -Command "New-Item -Path . -Name dummy.go -ItemType file -Force"
|
||||
- powershell.exe -Command "Add-Content -Path .\dummy.go -Value 'package main'"
|
||||
- powershell.exe -Command "Add-Content -Path .\dummy.go -Value 'func main() {}'"
|
||||
|
||||
builds:
|
||||
- id: komorebi
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebi
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi.exe" ".\dist\komorebi_windows_amd64_v1\komorebi.exe"
|
||||
- id: komorebic
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebic
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64_v1\komorebic.exe"
|
||||
- id: komorebic-no-console
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebic-no-console
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic-no-console.exe" ".\dist\komorebic-no-console_windows_amd64_v1\komorebic-no-console.exe"
|
||||
- id: komorebi-gui
|
||||
main: dummy.go
|
||||
goos: [ "windows" ]
|
||||
goarch: [ "amd64" ]
|
||||
binary: komorebi-gui
|
||||
hooks:
|
||||
post:
|
||||
- mkdir -p dist/windows_amd64
|
||||
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-gui.exe" ".\dist\komorebi-gui_windows_amd64_v1\komorebi-gui.exe"
|
||||
- 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
|
||||
2182
Cargo.lock
generated
2182
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -17,8 +17,8 @@ chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.28"
|
||||
egui_extras = "0.28"
|
||||
eframe = "0.29"
|
||||
egui_extras = "0.29"
|
||||
dirs = "5"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
@@ -38,17 +38,21 @@ windows-implement = { version = "0.58" }
|
||||
windows-interface = { version = "0.58" }
|
||||
windows-core = { version = "0.58" }
|
||||
shadow-rs = "0.35"
|
||||
which = "6"
|
||||
which = "7"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.58"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
|
||||
24
README.md
24
README.md
@@ -60,13 +60,17 @@ channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
|
||||
_komorebi_ development videos. If you would like to be notified of upcoming
|
||||
videos please subscribe and turn on notifications.
|
||||
|
||||
There is an [Awesome List](https://github.com/LGUG2Z/awesome-komorebi) which
|
||||
showcases the many awesome projects that exist in the _komorebi_ ecosystem.
|
||||
|
||||
_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) before you
|
||||
consider sponsoring me on GitHub.
|
||||
Relief Fund](https://pcrf1.app.neoncrm.com/forms/gaza-recovery) or contributing
|
||||
to a [Gaza Funds campaign](https://gazafunds.com) before you consider sponsoring
|
||||
me on GitHub.
|
||||
|
||||
[GitHub Sponsors is enabled for this
|
||||
project](https://github.com/sponsors/LGUG2Z). Unfortunately I don't have
|
||||
@@ -96,7 +100,6 @@ video will answer the majority of your questions.
|
||||
|
||||
[](https://www.youtube.com/watch?v=0LCbS_gm0RA)
|
||||
|
||||
|
||||
# Demonstrations
|
||||
|
||||
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
|
||||
@@ -105,7 +108,6 @@ _komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komo
|
||||
|
||||
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
|
||||
|
||||
|
||||
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
|
||||
11 with a terminal emulator, a web browser and a code editor. The original
|
||||
video can be viewed
|
||||
@@ -194,13 +196,19 @@ required.
|
||||
`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.
|
||||
this means that you are free to do whatever you want with `komorebi` for
|
||||
personal use 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.
|
||||
|
||||
The [Komorebi 1.0.0 License](./LICENSE.md) does not permit any kind of
|
||||
commercial use.
|
||||
|
||||
A dedicated license and EULA will be introduced in 2025 for both commercial and
|
||||
noncommercial organizations.
|
||||
|
||||
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information about how
|
||||
code contributions to `komorebi` are licensed.
|
||||
|
||||
@@ -359,7 +367,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.29"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.30"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
|
||||
@@ -10,9 +10,8 @@ Options:
|
||||
Desired ease function for animation
|
||||
|
||||
[default: linear]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart,
|
||||
ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ,
|
||||
ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo,
|
||||
ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# application-specific-configuration-schema
|
||||
|
||||
```
|
||||
Generate a JSON Schema for applications.yaml
|
||||
Generate a JSON Schema for applications.json
|
||||
|
||||
Usage: komorebic.exe application-specific-configuration-schema
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Arguments:
|
||||
Options:
|
||||
-w, --window-kind <WINDOW_KIND>
|
||||
[default: single]
|
||||
[possible values: single, stack, monocle, unfocused]
|
||||
[possible values: single, stack, monocle, unfocused, floating]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
16
docs/cli/convert-app-specific-configuration.md
Normal file
16
docs/cli/convert-app-specific-configuration.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# convert-app-specific-configuration
|
||||
|
||||
```
|
||||
Convert a v1 ASC YAML file to a v2 ASC JSON file
|
||||
|
||||
Usage: komorebic.exe convert-app-specific-configuration <PATH>
|
||||
|
||||
Arguments:
|
||||
<PATH>
|
||||
YAML file from which the application-specific configurations should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -9,9 +9,6 @@ Options:
|
||||
-c, --config <CONFIG>
|
||||
Path to a static configuration JSON file
|
||||
|
||||
-f, --ffm
|
||||
Enable komorebi's custom focus-follows-mouse implementation
|
||||
|
||||
--whkd
|
||||
Enable autostart of whkd
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# fetch-app-specific-configuration
|
||||
|
||||
```
|
||||
Fetch the latest version of applications.yaml from komorebi-application-specific-configuration
|
||||
Fetch the latest version of applications.json from komorebi-application-specific-configuration
|
||||
|
||||
Usage: komorebic.exe fetch-app-specific-configuration
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# focus-follows-mouse
|
||||
|
||||
```
|
||||
Enable or disable focus follows mouse for the operating system
|
||||
|
||||
Usage: komorebic.exe focus-follows-mouse [OPTIONS] <BOOLEAN_STATE>
|
||||
|
||||
Arguments:
|
||||
<BOOLEAN_STATE>
|
||||
[possible values: enable, disable]
|
||||
|
||||
Options:
|
||||
-i, --implementation <IMPLEMENTATION>
|
||||
[default: windows]
|
||||
|
||||
Possible values:
|
||||
- komorebi: A custom FFM implementation (slightly more CPU-intensive)
|
||||
- windows: The native (legacy) Windows FFM implementation
|
||||
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
# format-app-specific-configuration
|
||||
|
||||
```
|
||||
Format a YAML file for use with the 'ahk-app-specific-configuration' command
|
||||
|
||||
Usage: komorebic.exe format-app-specific-configuration <PATH>
|
||||
|
||||
Arguments:
|
||||
<PATH>
|
||||
YAML file from which the application-specific configurations should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,9 +1,9 @@
|
||||
# float-rule
|
||||
# ignore-rule
|
||||
|
||||
```
|
||||
Add a rule to always float the specified application
|
||||
Add a rule to ignore the specified application
|
||||
|
||||
Usage: komorebic.exe float-rule <IDENTIFIER> <ID>
|
||||
Usage: komorebic.exe ignore-rule <IDENTIFIER> <ID>
|
||||
|
||||
Arguments:
|
||||
<IDENTIFIER>
|
||||
@@ -1,16 +0,0 @@
|
||||
# load-custom-layout
|
||||
|
||||
```
|
||||
Load a custom layout from file for the focused workspace
|
||||
|
||||
Usage: komorebic.exe load-custom-layout <PATH>
|
||||
|
||||
Arguments:
|
||||
<PATH>
|
||||
JSON or YAML file from which the custom layout definition should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
# named-workspace-custom-layout-rule
|
||||
|
||||
```
|
||||
Add a dynamic custom layout for the specified workspace
|
||||
|
||||
Usage: komorebic.exe named-workspace-custom-layout-rule <WORKSPACE> <AT_CONTAINER_COUNT> <PATH>
|
||||
|
||||
Arguments:
|
||||
<WORKSPACE>
|
||||
Target workspace name
|
||||
|
||||
<AT_CONTAINER_COUNT>
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<PATH>
|
||||
JSON or YAML file from which the custom layout definition should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
# named-workspace-custom-layout
|
||||
|
||||
```
|
||||
Set a custom layout for the specified workspace
|
||||
|
||||
Usage: komorebic.exe named-workspace-custom-layout <WORKSPACE> <PATH>
|
||||
|
||||
Arguments:
|
||||
<WORKSPACE>
|
||||
Target workspace name
|
||||
|
||||
<PATH>
|
||||
JSON or YAML file from which the custom layout definition should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -6,9 +6,6 @@ Start komorebi.exe as a background process
|
||||
Usage: komorebic.exe start [OPTIONS]
|
||||
|
||||
Options:
|
||||
-f, --ffm
|
||||
Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
|
||||
-c, --config <CONFIG>
|
||||
Path to a static configuration JSON file
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ Options:
|
||||
--whkd
|
||||
Stop whkd if it is running as a background process
|
||||
|
||||
--ahk
|
||||
Stop ahk if it is running as a background process
|
||||
|
||||
--bar
|
||||
Stop komorebi-bar if it is running as a background process
|
||||
|
||||
|
||||
12
docs/cli/toggle-float-override.md
Normal file
12
docs/cli/toggle-float-override.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-float-override
|
||||
|
||||
```
|
||||
Enable or disable float override, which makes it so every new window opens in floating mode
|
||||
|
||||
Usage: komorebic.exe toggle-float-override
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
# toggle-focus-follows-mouse
|
||||
|
||||
```
|
||||
Toggle focus follows mouse for the operating system
|
||||
|
||||
Usage: komorebic.exe toggle-focus-follows-mouse [OPTIONS]
|
||||
|
||||
Options:
|
||||
-i, --implementation <IMPLEMENTATION>
|
||||
[default: windows]
|
||||
|
||||
Possible values:
|
||||
- komorebi: A custom FFM implementation (slightly more CPU-intensive)
|
||||
- windows: The native (legacy) Windows FFM implementation
|
||||
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
```
|
||||
12
docs/cli/toggle-workspace-float-override.md
Normal file
12
docs/cli/toggle-workspace-float-override.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-workspace-float-override
|
||||
|
||||
```
|
||||
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace previously it takes the opposite of the global value
|
||||
|
||||
Usage: komorebic.exe toggle-workspace-float-override
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/toggle-workspace-window-container-behaviour.md
Normal file
12
docs/cli/toggle-workspace-window-container-behaviour.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-workspace-window-container-behaviour
|
||||
|
||||
```
|
||||
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the global value
|
||||
|
||||
Usage: komorebic.exe toggle-workspace-window-container-behaviour
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,25 +0,0 @@
|
||||
# workspace-custom-layout-rule
|
||||
|
||||
```
|
||||
Add a dynamic custom layout for the specified workspace
|
||||
|
||||
Usage: komorebic.exe workspace-custom-layout-rule <MONITOR> <WORKSPACE> <AT_CONTAINER_COUNT> <PATH>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<AT_CONTAINER_COUNT>
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<PATH>
|
||||
JSON or YAML file from which the custom layout definition should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
# workspace-custom-layout
|
||||
|
||||
```
|
||||
Set a custom layout for the specified workspace
|
||||
|
||||
Usage: komorebic.exe workspace-custom-layout <MONITOR> <WORKSPACE> <PATH>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<PATH>
|
||||
JSON or YAML file from which the custom layout definition should be loaded
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
29
docs/common-workflows/autostart.md
Normal file
29
docs/common-workflows/autostart.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Autostart
|
||||
|
||||
If you would like to autostart `komorebi`, you can use the `komorebic enable-autostart` command to generate a shortcut
|
||||
in the `shell:startup` folder.
|
||||
|
||||
```
|
||||
Generates the komorebi.lnk shortcut in shell:startup to autostart komorebi
|
||||
|
||||
Usage: komorebic.exe enable-autostart [OPTIONS]
|
||||
|
||||
Options:
|
||||
-c, --config <CONFIG>
|
||||
Path to a static configuration JSON file
|
||||
|
||||
-f, --ffm
|
||||
Enable komorebi's custom focus-follows-mouse implementation
|
||||
|
||||
--whkd
|
||||
Enable autostart of whkd
|
||||
|
||||
--ahk
|
||||
Enable autostart of ahk
|
||||
|
||||
--bar
|
||||
Enable autostart of komorebi-bar
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
```
|
||||
@@ -1,70 +0,0 @@
|
||||
# Custom Layouts
|
||||
|
||||
Particularly for users of ultrawide monitors, traditional tiling layouts may
|
||||
not seem like the most efficient use of screen space. If you feel this is the
|
||||
case with any of the default layouts, you are also welcome to create your own
|
||||
custom layouts and save them as JSON or YAML.
|
||||
|
||||
If you're not comfortable writing the layouts directly in JSON or YAML, you can
|
||||
use the [komorebi Custom Layout
|
||||
Generator](https://lgug2z.github.io/komorebi-custom-layout-generator/) to
|
||||
interactively define a custom layout, and then copy the generated JSON content.
|
||||
|
||||
Custom layouts can be loaded on the current workspace or configured for a
|
||||
specific workspace in the `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "personal",
|
||||
"custom_layout": "C:/Users/LGUG2Z/my-custom-layout.json"
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The fundamental building block of a custom _komorebi_ layout is the Column.
|
||||
|
||||
Columns come in three variants:
|
||||
|
||||
- **Primary**: This is where your primary focus will be on the screen most of
|
||||
the time. There must be exactly one Primary Column in any custom layout.
|
||||
Optionally, you can specify the percentage of the screen width that you want
|
||||
the Primary Column to occupy.
|
||||
- **Secondary**: This is an optional column that can either be full height of
|
||||
split horizontally into a fixed number of maximum rows. There can be any
|
||||
number of Secondary Columns in a custom layout.
|
||||
- **Tertiary**: This is the final column where any remaining windows will be
|
||||
split horizontally into rows as they get added.
|
||||
|
||||
If there is only one window on the screen when a custom layout is selected,
|
||||
that window will take up the full work area of the screen.
|
||||
|
||||
If the number of windows is equal to or less than the total number of columns
|
||||
defined in a custom layout, the windows will be arranged in an equal-width
|
||||
columns.
|
||||
|
||||
When the number of windows is greater than the number of columns defined in the
|
||||
custom layout, the windows will begin to be arranged according to the
|
||||
constraints set on the Primary and Secondary columns of the layout.
|
||||
|
||||
Here is an example custom layout that can be used as a starting point for your
|
||||
own:
|
||||
|
||||
```yaml
|
||||
- column: Secondary
|
||||
configuration: !Horizontal 2 # max number of rows
|
||||
- column: Primary
|
||||
configuration: !WidthPercentage 50 # percentage of screen
|
||||
- column: Tertiary
|
||||
configuration: Horizontal
|
||||
```
|
||||
|
||||
<!-- TODO: Record a new video -->
|
||||
|
||||
[](https://www.youtube.com/watch?v=SgmBHKEOcQ4)
|
||||
16
docs/common-workflows/floating-applications.md
Normal file
16
docs/common-workflows/floating-applications.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Floating Windows
|
||||
|
||||
Sometimes you will want a specific application to be managed as a floating window.
|
||||
You can add rules to enforce this behaviour in the `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"floating_applications": [
|
||||
{
|
||||
"kind": "Title",
|
||||
"id": "Media Player",
|
||||
"matching_strategy": "Equals"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,34 +0,0 @@
|
||||
# Focus Follows Mouse
|
||||
|
||||
`komorebi` supports two focus-follows-mouse implementations; the native Windows
|
||||
Xmouse implementation, which treats the desktop, the task bar, and the system
|
||||
tray as windows and switches focus to them eagerly, and a custom `komorebi`
|
||||
implementation, which only considers windows managed by `komorebi` as valid
|
||||
targets to switch focus to when moving the mouse.
|
||||
|
||||
To enable the `komorebi` implementation you must start the process with the
|
||||
`--ffm` flag to explicitly enable the feature. This is because the mouse
|
||||
tracking required for this feature significantly increases the CPU usage of the
|
||||
process (on my machine, it jumps from <1% to ~4~), and this CPU increase
|
||||
persists regardless of whether focus-follows-mouse is enabled or disabled at
|
||||
any given time via `komorebic`'s configuration commands.
|
||||
|
||||
If the `komorebi` process has been started with the `--ffm` flag, you can
|
||||
enable focus follows mouse behaviour in the `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"focus_follows_mouse": "Komorebi"
|
||||
}
|
||||
```
|
||||
|
||||
When calling any of the `komorebic` commands related to focus-follows-mouse
|
||||
functionality, the `windows` implementation will be chosen as the default
|
||||
implementation. You can optionally specify the `komorebi` implementation by
|
||||
passing it as an argument to the `--implementation` flag:
|
||||
|
||||
```powershell
|
||||
komorebic.exe toggle-focus-follows-mouse --implementation komorebi
|
||||
```
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ applications are [already generated for
|
||||
you](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
|
||||
|
||||
Sometimes you will want a specific application to never be tiled, and instead
|
||||
float all the time. You can add rules to enforce this behaviour in the
|
||||
be completely ignored. You can add rules to enforce this behaviour in the
|
||||
`komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"float_rules": [
|
||||
"ignore_rules": [
|
||||
{
|
||||
"kind": "Title",
|
||||
"id": "Media Player",
|
||||
|
||||
19
docs/common-workflows/multiple-bar-instances.md
Normal file
19
docs/common-workflows/multiple-bar-instances.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Multiple Bar Instances
|
||||
|
||||
If you would like to run multiple instances of `komorebi-bar` to target different monitors, it is possible to do so
|
||||
by maintaining multiple `komorebi.bar.json` configuration files and specifying their paths in the `bar_configurations`
|
||||
array in your `komorebi.json` configuration file.
|
||||
|
||||
```json
|
||||
{
|
||||
"bar_configurations": [
|
||||
"C:/Users/LGUG2Z/komorebi.bar.monitor1.json",
|
||||
"C:/Users/LGUG2Z/komorebi.bar.monitor2.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You may also use `$Env:USERPROFILE` or `$Env:KOMOREBI_CONFIG_HOME` when specifying the paths.
|
||||
|
||||
The main difference between different `komorebi.bar.json` files will be the value of `monitor.index` which is used to
|
||||
target the monitor for each instance of `komorebi-bar`.
|
||||
@@ -62,7 +62,7 @@ using `default_workspace_padding` and `default_container_padding`.
|
||||
|
||||
You may have seen videos and screenshots of people using `komorebi` with a
|
||||
thick, colourful active window border. You can also enable this by setting
|
||||
`active_window_border` to `true`. However, please be warned that this feature
|
||||
`border` to `true`. However, please be warned that this feature
|
||||
is a crude hack trying to compensate for the insistence of Microsoft Windows
|
||||
design teams to make custom borders with widths that are actually visible to
|
||||
the user a thing of the past and removing this capability from the Win32 API.
|
||||
@@ -162,6 +162,8 @@ If you have an ultrawide monitor, I recommend using this layout.
|
||||
|
||||
If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layouts) this is almost exactly the same!
|
||||
|
||||
The `grid` layout does not support resizing windows tiles.
|
||||
|
||||
```
|
||||
+-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||
| | | | | | | | | | | | | | |
|
||||
@@ -177,7 +179,7 @@ If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layou
|
||||
|
||||
`whkd` is a fairly basic piece of software with a simple configuration format:
|
||||
key bindings go to the left of the colon, and shell commands go to the right of the
|
||||
colon. By default, the `whkdrc` file should be located in the `$Env:USERPROFILE/.config/` directory.
|
||||
colon.
|
||||
|
||||
Please remember that `whkd` does not support overriding Microsoft's limitations
|
||||
on hotkey bindings that include the `Windows` key. If this is important to you,
|
||||
@@ -188,6 +190,25 @@ bindings for `komorebic` commands instead.
|
||||
{% include "./whkdrc.sample" %}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
`whkd` searches for a `whkdrc` configuration file in the following locations:
|
||||
|
||||
* `$Env:WHKD_CONFIG_HOME`
|
||||
* `$Env:USERPROFILE/.config`
|
||||
|
||||
It is also possible to change a hotkey behavior depending on which application has focus:
|
||||
|
||||
```
|
||||
alt + n [
|
||||
# ProcessName as shown by `Get-Process`
|
||||
Firefox : echo "hello firefox"
|
||||
|
||||
# Spaces are fine, no quotes required
|
||||
Google Chrome : echo "hello chrome"
|
||||
]
|
||||
```
|
||||
|
||||
### Setting .shell
|
||||
|
||||
There is one special directive at the top of the file, `.shell` which can be
|
||||
|
||||
@@ -134,6 +134,26 @@ an offline machine to install.
|
||||
Once installed, proceed to get the [example configurations](example-configurations.md) (none of the commands for
|
||||
first-time set up and running komorebi require an internet connection).
|
||||
|
||||
## Upgrades
|
||||
|
||||
Before upgrading, make sure to run `komorebic stop --whkd --bar`. This is to ensure that all the current
|
||||
komorebi-related exe files can be replaced without issue.
|
||||
|
||||
Then, depending on whether you installed via `scoop` or `winget`, you can run the appropriate command:
|
||||
|
||||
```powershell
|
||||
# for winget
|
||||
winget upgrade LGUG2Z.komorebi
|
||||
```
|
||||
|
||||
```powershell
|
||||
# for scoop
|
||||
scoop update komorebi
|
||||
```
|
||||
|
||||
Once the upgrade is completed you can confirm that you have the latest version by running `komorebic --version`, and
|
||||
then start it with `komorebic start --whkd --bar`.
|
||||
|
||||
## Uninstallation
|
||||
|
||||
Before uninstalling, first run `komorebic stop --whkd --bar` to make sure that
|
||||
@@ -147,7 +167,7 @@ files created by the `quickstart` command and any other runtime files:
|
||||
|
||||
```powershell
|
||||
rm $Env:USERPROFILE\komorebi.json
|
||||
rm $Env:USERPROFILE\applications.yaml
|
||||
rm $Env:USERPROFILE\applications.json
|
||||
rm $Env:USERPROFILE\.config\whkdrc
|
||||
rm -r -Force $Env:LOCALAPPDATA\komorebi
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.bar.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.bar.json",
|
||||
"monitor": {
|
||||
"index": 0,
|
||||
"work_area_offset": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.30/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
"default_workspace_padding": 20,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# v0.1.22
|
||||
|
||||
In addition to the [changelog](https://github.com/LGUG2Z/komorebi/releases/tag/v0.1.22) of new features and fixes,
|
||||
please note the following changes from `v0.1.21` to adjust your configuration files accordingly.
|
||||
|
||||
## tl;dr
|
||||
|
||||
The way windows are sized and drawn has been improved to remove the need to manually specify and remove invisible
|
||||
borders for applications that overflow them. If you use the active window border, the first time you launch `v0.1.22`
|
||||
you may end up with a _huge_ border due to these changes.
|
||||
|
||||
`active_window_border_width` and `active_window_border_offset` have been renamed to `border_width` and `border_offset`
|
||||
as they now also apply outside the context of the active window border.
|
||||
|
||||
```json
|
||||
{
|
||||
"active_window_border": true,
|
||||
"border_width": 8,
|
||||
"border_offset": -1
|
||||
}
|
||||
```
|
||||
|
||||
Users of the active window border should start from these settings and read the notes below before making further
|
||||
adjustments.
|
||||
|
||||
## Changes to `active_window_border`, and window sizing:
|
||||
|
||||
- The border no longer creates a second drop-shadow around the active window
|
||||
- Windows are now sized to fill the layout region entirely, ignoring window decorations such as drop shadows
|
||||
- Border offset now starts exactly at the paint edge of the window on all sides
|
||||
- Windows are sized such that the border offset and border width are taken into account
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
### Gapless
|
||||
|
||||
- Disable "transparency effects" Personalization > Colors
|
||||
- Set the following settings in `komorebi.json`:
|
||||
```json
|
||||
{
|
||||
"default_workspace_padding": 0,
|
||||
"default_container_padding": 0,
|
||||
"border_offset": -1,
|
||||
"border_width": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 1px border
|
||||
|
||||
A 1px border is drawn around the window edge. Users may see a gap for a single pixel, if the system theme has a
|
||||
transparent edge - this is the windows themed edge, and is not present for all applications.
|
||||
|
||||
```json
|
||||
{
|
||||
"border_offset": 0,
|
||||
"border_width": 1
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,15 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Phantom Tiles
|
||||
|
||||
Sometimes you may experience an application which leaves "ghost tiles" on a workspace, where there is space reserved for
|
||||
a window but no window visible.
|
||||
|
||||
You can ignore these windows by following these steps:
|
||||
|
||||
* Run `komorebic visible-windows` to find details about the invisible window
|
||||
* Using that information, [create a rule to ignore that window](common-workflows/ignore-windows.md)
|
||||
|
||||
## AutoHotKey executable not found
|
||||
|
||||
If you try to start komorebi with AHK using `komorebic start --ahk`, and you have
|
||||
@@ -85,10 +95,10 @@ running `komorebic stop` and `komorebic start`.
|
||||
|
||||
To avoid waiting an eternity:
|
||||
|
||||
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
|
||||
Settings_
|
||||
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
|
||||
Settings_
|
||||
|
||||
_Turn off the display: 1 minute_
|
||||
_Turn off the display: 1 minute_
|
||||
|
||||
Allow a minute for the display to reset, then once it actually shuts off
|
||||
allow for any additional time as prompted by your monitor for the cycle to
|
||||
|
||||
34
justfile
34
justfile
@@ -7,36 +7,36 @@ clean:
|
||||
fmt:
|
||||
cargo +nightly fmt
|
||||
cargo +stable clippy
|
||||
prettier --write README.md
|
||||
prettier --write .goreleaser.yml
|
||||
prettier --write .github/ISSUE_TEMPLATE/bug_report.yml
|
||||
prettier --write .github/ISSUE_TEMPLATE/config.yml
|
||||
prettier --write .github/ISSUE_TEMPLATE/feature_request.yml
|
||||
prettier --write .github/dependabot.yml
|
||||
prettier --write .github/FUNDING.yml
|
||||
prettier --write .github/workflows/windows.yaml
|
||||
|
||||
install-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
|
||||
|
||||
install-target target:
|
||||
cargo +stable install --path {{ target }} --locked
|
||||
|
||||
install:
|
||||
just install-target komorebic
|
||||
just install-target komorebic-no-console
|
||||
just install-target komorebi-gui
|
||||
just install-target komorebi-bar
|
||||
just install-target komorebi
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
|
||||
run:
|
||||
cargo +stable run --bin komorebi --locked
|
||||
run target:
|
||||
cargo +stable run --bin {{ target }} --locked
|
||||
|
||||
warn $RUST_LOG="warn":
|
||||
just run
|
||||
warn target $RUST_LOG="warn":
|
||||
just run {{ target }}
|
||||
|
||||
info $RUST_LOG="info":
|
||||
just run
|
||||
info target $RUST_LOG="info":
|
||||
just run {{ target }}
|
||||
|
||||
debug $RUST_LOG="debug":
|
||||
just run
|
||||
debug target $RUST_LOG="debug":
|
||||
just run {{ target }}
|
||||
|
||||
trace $RUST_LOG="trace":
|
||||
just run
|
||||
trace target $RUST_LOG="trace":
|
||||
just run {{ target }}
|
||||
|
||||
deadlock $RUST_LOG="trace":
|
||||
cargo +stable run --bin komorebi --locked --features deadlock_detection
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.30"
|
||||
version = "0.1.31"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -9,7 +9,6 @@ edition = "2021"
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
|
||||
atomic_float = "1"
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
@@ -17,22 +16,21 @@ crossbeam-channel = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui-phosphor = "0.6.0"
|
||||
egui-phosphor = "0.7"
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
netdev = "0.31"
|
||||
num = "0.4.3"
|
||||
num-derive = "0.4.2"
|
||||
num-traits = "0.2.19"
|
||||
random_word = { version = "0.4.3", features = ["en"] }
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
random_word = { version = "0.4", features = ["en"] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
starship-battery = "0.10"
|
||||
sysinfo = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
|
||||
@@ -1,13 +1,25 @@
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
use crate::komorebi::KomorebiNotificationState;
|
||||
use crate::process_hwnd;
|
||||
use crate::render::Color32Ext;
|
||||
use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::render::RenderExt;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::widget::WidgetConfig;
|
||||
use crate::DPI;
|
||||
use crate::BAR_HEIGHT;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_LEFT;
|
||||
use crate::MONITOR_RIGHT;
|
||||
use crate::MONITOR_TOP;
|
||||
use crossbeam_channel::Receiver;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Align2;
|
||||
use eframe::egui::Area;
|
||||
use eframe::egui::CentralPanel;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
@@ -16,13 +28,13 @@ use eframe::egui::FontDefinitions;
|
||||
use eframe::egui::FontFamily;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Id;
|
||||
use eframe::egui::Layout;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::Rgba;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::ViewportCommand;
|
||||
use eframe::egui::Visuals;
|
||||
use font_loader::system_fonts;
|
||||
use font_loader::system_fonts::FontPropertyBuilder;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
@@ -38,8 +50,10 @@ use std::sync::Arc;
|
||||
|
||||
pub struct Komobar {
|
||||
pub config: Arc<KomobarConfig>,
|
||||
pub render_config: Rc<RefCell<RenderConfig>>,
|
||||
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub center_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub rx_gui: Receiver<komorebi_client::Notification>,
|
||||
pub rx_config: Receiver<KomobarConfig>,
|
||||
@@ -146,16 +160,47 @@ impl Komobar {
|
||||
Self::add_custom_font(ctx, font_family);
|
||||
}
|
||||
|
||||
if let Some(viewport) = &config.viewport {
|
||||
let dpi = DPI.load(Ordering::SeqCst);
|
||||
if let Some(position) = viewport.position {
|
||||
let pos2 = Pos2::new(position.x / dpi, position.y / dpi);
|
||||
ctx.send_viewport_cmd(ViewportCommand::OuterPosition(pos2));
|
||||
let position = config.position.clone().unwrap_or(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
|
||||
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
|
||||
}),
|
||||
end: Some(Position {
|
||||
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
|
||||
y: BAR_HEIGHT,
|
||||
}),
|
||||
});
|
||||
|
||||
if let Some(hwnd) = process_hwnd() {
|
||||
let start = position.start.unwrap_or(Position {
|
||||
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
|
||||
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
|
||||
});
|
||||
|
||||
let end = position.end.unwrap_or(Position {
|
||||
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
|
||||
y: BAR_HEIGHT,
|
||||
});
|
||||
|
||||
if end.y == 0.0 {
|
||||
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
|
||||
}
|
||||
|
||||
if let Some(position) = viewport.inner_size {
|
||||
let vec2 = Vec2::new(position.x / dpi, position.y / dpi);
|
||||
ctx.send_viewport_cmd(ViewportCommand::InnerSize(vec2));
|
||||
let rect = komorebi_client::Rect {
|
||||
left: start.x as i32,
|
||||
top: start.y as i32,
|
||||
right: end.x as i32,
|
||||
bottom: end.y as i32,
|
||||
};
|
||||
|
||||
let window = komorebi_client::Window::from(hwnd);
|
||||
match window.set_position(&rect, false) {
|
||||
Ok(_) => {
|
||||
tracing::info!("updated bar position");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +252,30 @@ impl Komobar {
|
||||
}
|
||||
}
|
||||
|
||||
// apply rounding to the widgets
|
||||
if let Some(
|
||||
Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config),
|
||||
) = &config.grouping
|
||||
{
|
||||
if let Some(rounding) = config.rounding {
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.rounding = rounding.into();
|
||||
style.visuals.widgets.inactive.rounding = rounding.into();
|
||||
style.visuals.widgets.hovered.rounding = rounding.into();
|
||||
style.visuals.widgets.active.rounding = rounding.into();
|
||||
style.visuals.widgets.open.rounding = rounding.into();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let theme_color = *self.bg_color.borrow();
|
||||
|
||||
self.render_config
|
||||
.replace(config.new_renderconfig(theme_color));
|
||||
|
||||
self.bg_color
|
||||
.replace(theme_color.try_apply_alpha(config.transparency_alpha));
|
||||
|
||||
if let Some(font_size) = &config.font_size {
|
||||
tracing::info!("attempting to set custom font size: {font_size}");
|
||||
Self::set_font_size(ctx, *font_size);
|
||||
@@ -221,7 +290,17 @@ impl Komobar {
|
||||
if let WidgetConfig::Komorebi(config) = widget_config {
|
||||
komorebi_widget = Some(Komorebi::from(config));
|
||||
komorebi_widget_idx = Some(idx);
|
||||
side = Some(Side::Left);
|
||||
side = Some(Alignment::Left);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(center_widgets) = &config.center_widgets {
|
||||
for (idx, widget_config) in center_widgets.iter().enumerate() {
|
||||
if let WidgetConfig::Komorebi(config) = widget_config {
|
||||
komorebi_widget = Some(Komorebi::from(config));
|
||||
komorebi_widget_idx = Some(idx);
|
||||
side = Some(Alignment::Center);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +308,7 @@ impl Komobar {
|
||||
if let WidgetConfig::Komorebi(config) = widget_config {
|
||||
komorebi_widget = Some(Komorebi::from(config));
|
||||
komorebi_widget_idx = Some(idx);
|
||||
side = Some(Side::Right);
|
||||
side = Some(Alignment::Right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +318,14 @@ impl Komobar {
|
||||
.map(|config| config.as_boxed_bar_widget())
|
||||
.collect::<Vec<Box<dyn BarWidget>>>();
|
||||
|
||||
let mut center_widgets = match &config.center_widgets {
|
||||
Some(center_widgets) => center_widgets
|
||||
.iter()
|
||||
.map(|config| config.as_boxed_bar_widget())
|
||||
.collect::<Vec<Box<dyn BarWidget>>>(),
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let mut right_widgets = config
|
||||
.right_widgets
|
||||
.iter()
|
||||
@@ -263,20 +350,23 @@ impl Komobar {
|
||||
|
||||
let boxed: Box<dyn BarWidget> = Box::new(widget);
|
||||
match side {
|
||||
Side::Left => left_widgets[idx] = boxed,
|
||||
Side::Right => right_widgets[idx] = boxed,
|
||||
Alignment::Left => left_widgets[idx] = boxed,
|
||||
Alignment::Center => center_widgets[idx] = boxed,
|
||||
Alignment::Right => right_widgets[idx] = boxed,
|
||||
}
|
||||
}
|
||||
|
||||
right_widgets.reverse();
|
||||
|
||||
self.left_widgets = left_widgets;
|
||||
self.center_widgets = center_widgets;
|
||||
self.right_widgets = right_widgets;
|
||||
|
||||
tracing::info!("widget configuration options applied");
|
||||
|
||||
self.komorebi_notification_state = komorebi_notification_state;
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
@@ -285,8 +375,10 @@ impl Komobar {
|
||||
) -> Self {
|
||||
let mut komobar = Self {
|
||||
config: config.clone(),
|
||||
render_config: Rc::new(RefCell::new(RenderConfig::new())),
|
||||
komorebi_notification_state: None,
|
||||
left_widgets: vec![],
|
||||
center_widgets: vec![],
|
||||
right_widgets: vec![],
|
||||
rx_gui,
|
||||
rx_config,
|
||||
@@ -294,6 +386,8 @@ impl Komobar {
|
||||
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
|
||||
};
|
||||
|
||||
komobar.apply_config(&cc.egui_ctx, &config, None);
|
||||
// needs a double apply the first time for some reason
|
||||
komobar.apply_config(&cc.egui_ctx, &config, None);
|
||||
|
||||
komobar
|
||||
@@ -353,13 +447,10 @@ impl Komobar {
|
||||
}
|
||||
}
|
||||
impl eframe::App for Komobar {
|
||||
// TODO: I think this is needed for transparency??
|
||||
// fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
|
||||
// egui::Rgba::TRANSPARENT.to_array()
|
||||
// let mut background = Color32::from_gray(18).to_normalized_gamma_f32();
|
||||
// background[3] = 0.9;
|
||||
// background
|
||||
// }
|
||||
// Needed for transparency
|
||||
fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
|
||||
Rgba::TRANSPARENT.to_array()
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
|
||||
@@ -401,17 +492,54 @@ impl eframe::App for Komobar {
|
||||
Frame::none().fill(*self.bg_color.borrow())
|
||||
};
|
||||
|
||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
|
||||
for w in &mut self.left_widgets {
|
||||
w.render(ctx, ui);
|
||||
}
|
||||
});
|
||||
let mut render_config = self.render_config.borrow_mut();
|
||||
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
for w in &mut self.right_widgets {
|
||||
w.render(ctx, ui);
|
||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||
// Apply grouping logic for the bar as a whole
|
||||
render_config.clone().apply_on_bar(ui, |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
// Left-aligned widgets layout
|
||||
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
|
||||
let mut render_conf = *render_config;
|
||||
render_conf.alignment = Some(Alignment::Left);
|
||||
|
||||
render_config.apply_on_alignment(ui, |ui| {
|
||||
for w in &mut self.left_widgets {
|
||||
w.render(ctx, ui, &mut render_conf);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Right-aligned widgets layout
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
let mut render_conf = *render_config;
|
||||
render_conf.alignment = Some(Alignment::Right);
|
||||
|
||||
render_config.apply_on_alignment(ui, |ui| {
|
||||
for w in &mut self.right_widgets {
|
||||
w.render(ctx, ui, &mut render_conf);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if !self.center_widgets.is_empty() {
|
||||
// Floating center widgets
|
||||
Area::new(Id::new("center_panel"))
|
||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0]) // Align in the center of the window
|
||||
.show(ctx, |ui| {
|
||||
Frame::none().show(ui, |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
let mut render_conf = *render_config;
|
||||
render_conf.alignment = Some(Alignment::Center);
|
||||
|
||||
render_config.apply_on_alignment(ui, |ui| {
|
||||
for w in &mut self.center_widgets {
|
||||
w.render(ctx, ui, &mut render_conf);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -420,7 +548,8 @@ impl eframe::App for Komobar {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Side {
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
@@ -23,6 +24,8 @@ pub struct BatteryConfig {
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<BatteryConfig> for Battery {
|
||||
@@ -30,6 +33,7 @@ impl From<BatteryConfig> for Battery {
|
||||
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) {
|
||||
@@ -40,7 +44,12 @@ impl From<BatteryConfig> for Battery {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
last_state = format!("{percentage}%");
|
||||
last_state = match prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +58,7 @@ impl From<BatteryConfig> for Battery {
|
||||
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(),
|
||||
}
|
||||
@@ -65,6 +75,7 @@ pub struct Battery {
|
||||
manager: Manager,
|
||||
pub state: BatteryState,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_state: String,
|
||||
last_updated: Instant,
|
||||
}
|
||||
@@ -86,7 +97,12 @@ impl Battery {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
output = format!("{percentage:.0}%");
|
||||
output = match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage:.0}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +115,7 @@ impl Battery {
|
||||
}
|
||||
|
||||
impl BarWidget for Battery {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
@@ -116,7 +132,10 @@ impl BarWidget for Battery {
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
emoji.to_string(),
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
@@ -128,14 +147,14 @@ impl BarWidget for Battery {
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
ui.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::render::Grouping;
|
||||
use crate::widget::WidgetConfig;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
@@ -7,14 +8,16 @@ use komorebi_client::Rect;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.30`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.31`
|
||||
pub struct KomobarConfig {
|
||||
/// Viewport options (see: https://docs.rs/egui/latest/egui/viewport/struct.ViewportBuilder.html)
|
||||
pub viewport: Option<ViewportConfig>,
|
||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)
|
||||
/// Bar positioning options
|
||||
#[serde(alias = "viewport")]
|
||||
pub position: Option<PositionConfig>,
|
||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
|
||||
pub frame: Option<FrameConfig>,
|
||||
/// Monitor options
|
||||
pub monitor: MonitorConfig,
|
||||
@@ -26,18 +29,57 @@ pub struct KomobarConfig {
|
||||
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>,
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn aliases(raw: &str) {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("position", ["viewport"]);
|
||||
map.insert("end", ["inner_frame"]);
|
||||
|
||||
let mut display = false;
|
||||
|
||||
for aliases in map.values() {
|
||||
for a in aliases {
|
||||
if raw.contains(a) {
|
||||
display = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if display {
|
||||
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
|
||||
for (canonical, aliases) in map {
|
||||
for alias in aliases {
|
||||
if raw.contains(alias) {
|
||||
println!(r#""{alias}" is now "{canonical}""#);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ViewportConfig {
|
||||
pub struct PositionConfig {
|
||||
/// The desired starting position of the bar (0,0 = top left of the screen)
|
||||
pub position: Option<Position>,
|
||||
#[serde(alias = "position")]
|
||||
pub start: Option<Position>,
|
||||
/// The desired size of the bar from the starting position (usually monitor width x desired height)
|
||||
pub inner_size: Option<Position>,
|
||||
#[serde(alias = "inner_size")]
|
||||
pub end: Option<Position>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -103,11 +145,13 @@ impl From<Position> for Pos2 {
|
||||
pub enum KomobarTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
|
||||
name: komorebi_themes::Base16,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
@@ -131,3 +175,25 @@ impl From<KomorebiTheme> for KomobarTheme {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum LabelPrefix {
|
||||
/// Show no prefix
|
||||
None,
|
||||
/// Show an icon
|
||||
Icon,
|
||||
/// Show text
|
||||
Text,
|
||||
/// Show an icon and text
|
||||
IconAndText,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum DisplayFormat {
|
||||
/// Show only icon
|
||||
Icon,
|
||||
/// Show only text
|
||||
Text,
|
||||
/// Show both icon and text
|
||||
IconAndText,
|
||||
}
|
||||
|
||||
121
komorebi-bar/src/cpu.rs
Normal file
121
komorebi-bar/src/cpu.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::RefreshKind;
|
||||
use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CpuConfig {
|
||||
/// Enable the Cpu widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<CpuConfig> for Cpu {
|
||||
fn from(value: CpuConfig) -> Self {
|
||||
let mut system =
|
||||
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
|
||||
|
||||
system.refresh_cpu_usage();
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Cpu {
|
||||
pub enable: bool,
|
||||
system: System,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Cpu {
|
||||
fn output(&mut self) -> String {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
self.system.refresh_cpu_usage();
|
||||
self.last_updated = now;
|
||||
}
|
||||
|
||||
let used = self.system.global_cpu_usage();
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Cpu {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CPU.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
@@ -19,6 +20,8 @@ pub struct DateConfig {
|
||||
pub enable: bool,
|
||||
/// Set the Date format
|
||||
pub format: DateFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<DateConfig> for Date {
|
||||
@@ -26,6 +29,7 @@ impl From<DateConfig> for Date {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,6 +74,7 @@ impl DateFormat {
|
||||
pub struct Date {
|
||||
pub enable: bool,
|
||||
pub format: DateFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
}
|
||||
|
||||
impl Date {
|
||||
@@ -81,9 +86,9 @@ impl Date {
|
||||
}
|
||||
|
||||
impl BarWidget for Date {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
@@ -93,31 +98,40 @@ impl BarWidget for Date {
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
egui_phosphor::regular::CALENDAR_DOTS.to_string(),
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CALENDAR_DOTS.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.insert_str(0, "DATE: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.next()
|
||||
}
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
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,36 +1,44 @@
|
||||
use crate::bar::apply_theme;
|
||||
use crate::config::DisplayFormat;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::komorebi_layout::KomorebiLayout;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::WIDGET_SPACING;
|
||||
use crate::MONITOR_INDEX;
|
||||
use crossbeam_channel::Receiver;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use eframe::egui::vec2;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ColorImage;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Image;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::SelectableLabel;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Rounding;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::TextureHandle;
|
||||
use eframe::egui::TextureOptions;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::CycleDirection;
|
||||
use komorebi_client::Container;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::Window;
|
||||
use komorebi_client::Workspace;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -53,20 +61,28 @@ pub struct KomorebiWorkspacesConfig {
|
||||
pub enable: bool,
|
||||
/// Hide workspaces without any windows
|
||||
pub hide_empty_workspaces: bool,
|
||||
/// Display format of the workspace
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiLayoutConfig {
|
||||
/// Enable the Komorebi Layout widget
|
||||
pub enable: bool,
|
||||
/// List of layout options
|
||||
pub options: Option<Vec<KomorebiLayout>>,
|
||||
/// Display format of the current layout
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct KomorebiFocusedWindowConfig {
|
||||
/// Enable the Komorebi Focused Window widget
|
||||
pub enable: bool,
|
||||
/// Show the icon of the currently focused window
|
||||
pub show_icon: bool,
|
||||
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused window)
|
||||
pub show_icon: Option<bool>,
|
||||
/// Display format of the currently focused window
|
||||
pub display: Option<DisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -83,14 +99,6 @@ impl From<&KomorebiConfig> for Komorebi {
|
||||
if let Some(configuration_switcher) = &value.configuration_switcher {
|
||||
let mut configuration_switcher = configuration_switcher.clone();
|
||||
for (_, location) in configuration_switcher.configurations.iter_mut() {
|
||||
if let Ok(expanded) = std::env::var("KOMOREBI_CONFIG_HOME") {
|
||||
*location = location.replace("$Env:KOMOREBI_CONFIG_HOME", &expanded);
|
||||
}
|
||||
|
||||
if let Ok(expanded) = std::env::var("USERPROFILE") {
|
||||
*location = location.replace("$Env:USERPROFILE", &expanded);
|
||||
}
|
||||
|
||||
*location = dunce::simplified(&PathBuf::from(location.clone()))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
@@ -108,11 +116,12 @@ impl From<&KomorebiConfig> for Komorebi {
|
||||
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
|
||||
mouse_follows_focus: true,
|
||||
work_area_offset: None,
|
||||
focused_container_information: (vec![], vec![], 0),
|
||||
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
|
||||
stack_accent: None,
|
||||
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
|
||||
})),
|
||||
workspaces: value.workspaces,
|
||||
layout: value.layout,
|
||||
layout: value.layout.clone(),
|
||||
focused_window: value.focused_window,
|
||||
configuration_switcher,
|
||||
}
|
||||
@@ -129,233 +138,89 @@ pub struct Komorebi {
|
||||
}
|
||||
|
||||
impl BarWidget for Komorebi {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
|
||||
|
||||
if self.workspaces.enable {
|
||||
let mut update = None;
|
||||
|
||||
for (i, ws) in komorebi_notification_state.workspaces.iter().enumerate() {
|
||||
if ui
|
||||
.add(SelectableLabel::new(
|
||||
komorebi_notification_state.selected_workspace.eq(ws),
|
||||
ws.to_string(),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
update = Some(ws.to_string());
|
||||
let mut proceed = true;
|
||||
if !komorebi_notification_state.workspaces.is_empty() {
|
||||
let format = self.workspaces.display.unwrap_or(DisplayFormat::Text);
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
|
||||
.is_err()
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for (i, (ws, container_information)) in
|
||||
komorebi_notification_state.workspaces.iter().enumerate()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
|
||||
proceed = false;
|
||||
}
|
||||
if SelectableFrame::new(
|
||||
komorebi_notification_state.selected_workspace.eq(ws),
|
||||
)
|
||||
.show(ui, |ui| {
|
||||
let mut has_icon = false;
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: FocusWorkspaceNumber");
|
||||
proceed = false;
|
||||
}
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
|
||||
let icons: Vec<_> =
|
||||
container_information.icons.iter().flatten().collect();
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed && komorebi_client::send_message(&SocketMessage::Retile).is_err() {
|
||||
tracing::error!("could not send message to komorebi: Retile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(update) = update {
|
||||
komorebi_notification_state.selected_workspace = update;
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if let Some(layout) = self.layout {
|
||||
if layout.enable {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(komorebi_notification_state.layout.to_string())
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
match komorebi_notification_state.layout {
|
||||
KomorebiLayout::Default(_) => {
|
||||
if komorebi_client::send_message(&SocketMessage::CycleLayout(
|
||||
CycleDirection::Next,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: CycleLayout");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Floating => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: ToggleTiling");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Paused => {
|
||||
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
|
||||
tracing::error!("could not send message to komorebi: TogglePause");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Custom => {}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(configuration_switcher) = &self.configuration_switcher {
|
||||
if configuration_switcher.enable {
|
||||
for (name, location) in configuration_switcher.configurations.iter() {
|
||||
let path = PathBuf::from(location);
|
||||
if path.is_file()
|
||||
&& ui
|
||||
.add(Label::new(name).selectable(false).sense(Sense::click()))
|
||||
.clicked()
|
||||
{
|
||||
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
|
||||
let mut proceed = true;
|
||||
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
|
||||
canonicalized,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: ReplaceConfiguration"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if let Some(rect) = komorebi_notification_state.work_area_offset {
|
||||
if proceed {
|
||||
match komorebi_client::send_query(&SocketMessage::Query(
|
||||
komorebi_client::StateQuery::FocusedMonitorIndex,
|
||||
)) {
|
||||
Ok(idx) => {
|
||||
if let Ok(monitor_idx) = idx.parse::<usize>() {
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MonitorWorkAreaOffset"
|
||||
if !icons.is_empty() {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::same(
|
||||
ui.style().spacing.button_padding.y,
|
||||
))
|
||||
.show(ui, |ui| {
|
||||
for icon in icons {
|
||||
ui.add(
|
||||
Image::from(&img_to_texture(ctx, icon))
|
||||
.maintain_aspect_ratio(true)
|
||||
.shrink_to_fit(),
|
||||
);
|
||||
|
||||
if !has_icon {
|
||||
has_icon = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: Query"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
// draw a custom icon when there is no app icon
|
||||
if match format {
|
||||
DisplayFormat::Icon => !has_icon,
|
||||
_ => false,
|
||||
} {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
if let Some(focused_window) = self.focused_window {
|
||||
if focused_window.enable {
|
||||
let titles = &komorebi_notification_state.focused_container_information.0;
|
||||
let icons = &komorebi_notification_state.focused_container_information.1;
|
||||
let focused_window_idx =
|
||||
komorebi_notification_state.focused_container_information.2;
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::splat(font_id.size), Sense::hover());
|
||||
let stroke =
|
||||
Stroke::new(1.0, ctx.style().visuals.selection.stroke.color);
|
||||
let mut rect = response.rect;
|
||||
let rounding = Rounding::same(rect.width() * 0.1);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke);
|
||||
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
|
||||
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
|
||||
for (i, (title, icon)) in iter.enumerate() {
|
||||
if focused_window.show_icon {
|
||||
if let Some(img) = icon {
|
||||
ui.add(
|
||||
Image::from(&img_to_texture(ctx, img))
|
||||
.maintain_aspect_ratio(true)
|
||||
.max_height(15.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if i == focused_window_idx {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let layout_job = LayoutJob::simple(
|
||||
title.to_string(),
|
||||
font_id.clone(),
|
||||
komorebi_notification_state
|
||||
.stack_accent
|
||||
.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
if titles.len() > 1 {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job).selectable(false).truncate(),
|
||||
);
|
||||
} else {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title).selectable(false).truncate(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
if custom_ui
|
||||
.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title)
|
||||
.selectable(false)
|
||||
.sense(Sense::click())
|
||||
.truncate(),
|
||||
)
|
||||
.clicked()
|
||||
response.on_hover_text(ws.to_string())
|
||||
} else if match format {
|
||||
DisplayFormat::Icon => has_icon,
|
||||
_ => false,
|
||||
} {
|
||||
ui.response().on_hover_text(ws.to_string())
|
||||
} else {
|
||||
ui.add(Label::new(ws.to_string()).selectable(false))
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
update = Some(ws.to_string());
|
||||
let mut proceed = true;
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
false,
|
||||
))
|
||||
@@ -364,33 +229,235 @@ impl BarWidget for Komorebi {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(i))
|
||||
if proceed
|
||||
&& komorebi_client::send_message(
|
||||
&SocketMessage::FocusMonitorWorkspaceNumber(
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusStackWindow"
|
||||
"could not send message to komorebi: FocusWorkspaceNumber"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(
|
||||
&SocketMessage::RetileWithResizeDimensions,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: Retile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
if let Some(update) = update {
|
||||
komorebi_notification_state.selected_workspace = update;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(layout_config) = &self.layout {
|
||||
if layout_config.enable {
|
||||
let workspace_idx: Option<usize> = komorebi_notification_state
|
||||
.workspaces
|
||||
.iter()
|
||||
.position(|o| komorebi_notification_state.selected_workspace.eq(&o.0));
|
||||
|
||||
komorebi_notification_state.layout.show(
|
||||
ctx,
|
||||
ui,
|
||||
config,
|
||||
layout_config,
|
||||
workspace_idx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(configuration_switcher) = &self.configuration_switcher {
|
||||
if configuration_switcher.enable {
|
||||
for (name, location) in configuration_switcher.configurations.iter() {
|
||||
let path = PathBuf::from(location);
|
||||
if path.is_file() {
|
||||
config.apply_on_widget(false, ui,|ui|{
|
||||
if SelectableFrame::new(false).show(ui, |ui|{
|
||||
ui.add(Label::new(name).selectable(false))
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
|
||||
let mut proceed = true;
|
||||
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
|
||||
canonicalized,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: ReplaceConfiguration"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if let Some(rect) = komorebi_notification_state.work_area_offset {
|
||||
if proceed {
|
||||
match komorebi_client::send_query(&SocketMessage::Query(
|
||||
komorebi_client::StateQuery::FocusedMonitorIndex,
|
||||
)) {
|
||||
Ok(idx) => {
|
||||
if let Ok(monitor_idx) = idx.parse::<usize>() {
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MonitorWorkAreaOffset"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: Query"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(focused_window) = self.focused_window {
|
||||
if focused_window.enable {
|
||||
let titles = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.titles;
|
||||
if !titles.is_empty() {
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
let icons = &komorebi_notification_state
|
||||
.focused_container_information
|
||||
.icons;
|
||||
let focused_window_idx = komorebi_notification_state
|
||||
.focused_container_information
|
||||
.focused_window_idx;
|
||||
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
|
||||
for (i, (title, icon)) in iter.enumerate() {
|
||||
let selected = i == focused_window_idx;
|
||||
|
||||
if SelectableFrame::new(selected)
|
||||
.show(ui, |ui| {
|
||||
// handle legacy setting
|
||||
let format = focused_window.display.unwrap_or(
|
||||
if focused_window.show_icon.unwrap_or(false) {
|
||||
DisplayFormat::IconAndText
|
||||
} else {
|
||||
DisplayFormat::Text
|
||||
},
|
||||
);
|
||||
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format
|
||||
{
|
||||
if let Some(img) = icon {
|
||||
Frame::none()
|
||||
.inner_margin(Margin::same(
|
||||
ui.style().spacing.button_padding.y,
|
||||
))
|
||||
.show(ui, |ui| {
|
||||
let response = ui.add(
|
||||
Image::from(&img_to_texture(ctx, img))
|
||||
.maintain_aspect_ratio(true)
|
||||
.shrink_to_fit(),
|
||||
);
|
||||
|
||||
if let DisplayFormat::Icon = format {
|
||||
response.on_hover_text(title);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let DisplayFormat::Text | DisplayFormat::IconAndText = format
|
||||
{
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title).selectable(false).truncate(),
|
||||
);
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
if selected {
|
||||
return;
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
false,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(
|
||||
i,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusStackWindow"
|
||||
);
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,33 +471,15 @@ fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationState {
|
||||
pub workspaces: Vec<String>,
|
||||
pub workspaces: Vec<(String, KomorebiNotificationStateContainerInformation)>,
|
||||
pub selected_workspace: String,
|
||||
pub focused_container_information: (Vec<String>, Vec<Option<RgbaImage>>, usize),
|
||||
pub focused_container_information: KomorebiNotificationStateContainerInformation,
|
||||
pub layout: KomorebiLayout,
|
||||
pub hide_empty_workspaces: bool,
|
||||
pub mouse_follows_focus: bool,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub stack_accent: Option<Color32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum KomorebiLayout {
|
||||
Default(komorebi_client::DefaultLayout),
|
||||
Floating,
|
||||
Paused,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl Display for KomorebiLayout {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
|
||||
KomorebiLayout::Floating => write!(f, "Floating"),
|
||||
KomorebiLayout::Paused => write!(f, "Paused"),
|
||||
KomorebiLayout::Custom => write!(f, "Custom"),
|
||||
}
|
||||
}
|
||||
pub monitor_index: usize,
|
||||
}
|
||||
|
||||
impl KomorebiNotificationState {
|
||||
@@ -445,94 +494,152 @@ impl KomorebiNotificationState {
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
) {
|
||||
if let Ok(notification) = rx_gui.try_recv() {
|
||||
if let NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(path)) =
|
||||
notification.event
|
||||
{
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
match rx_gui.try_recv() {
|
||||
Err(error) => match error {
|
||||
TryRecvError::Empty => {}
|
||||
TryRecvError::Disconnected => {
|
||||
tracing::error!(
|
||||
"failed to receive komorebi notification on gui thread: {error}"
|
||||
);
|
||||
}
|
||||
},
|
||||
Ok(notification) => {
|
||||
match notification.event {
|
||||
NotificationEvent::WindowManager(_) => {}
|
||||
NotificationEvent::Socket(message) => match message {
|
||||
SocketMessage::ReloadStaticConfiguration(path) => {
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(ctx, KomobarTheme::from(theme), bg_color.clone());
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
|
||||
self.monitor_index = monitor_index;
|
||||
|
||||
self.mouse_follows_focus = notification.state.mouse_follows_focus;
|
||||
|
||||
let monitor = ¬ification.state.monitors.elements()[monitor_index];
|
||||
self.work_area_offset =
|
||||
notification.state.monitors.elements()[monitor_index].work_area_offset();
|
||||
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
|
||||
let mut workspaces = vec![];
|
||||
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
|
||||
|
||||
for (i, ws) in monitor.workspaces().iter().enumerate() {
|
||||
let should_show = if self.hide_empty_workspaces {
|
||||
focused_workspace_idx == i || !ws.containers().is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_show {
|
||||
workspaces.push((
|
||||
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
|
||||
ws.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.mouse_follows_focus = notification.state.mouse_follows_focus;
|
||||
self.workspaces = workspaces;
|
||||
|
||||
let monitor = ¬ification.state.monitors.elements()[monitor_index];
|
||||
self.work_area_offset =
|
||||
notification.state.monitors.elements()[monitor_index].work_area_offset();
|
||||
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
|
||||
let mut workspaces = vec![];
|
||||
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
|
||||
|
||||
for (i, ws) in monitor.workspaces().iter().enumerate() {
|
||||
let should_add = if self.hide_empty_workspaces {
|
||||
focused_workspace_idx == i || !ws.containers().is_empty()
|
||||
if monitor.workspaces()[focused_workspace_idx]
|
||||
.monocle_container()
|
||||
.is_some()
|
||||
{
|
||||
self.layout = KomorebiLayout::Monocle;
|
||||
} else if !*monitor.workspaces()[focused_workspace_idx].tile() {
|
||||
self.layout = KomorebiLayout::Floating;
|
||||
} else if notification.state.is_paused {
|
||||
self.layout = KomorebiLayout::Paused;
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_add {
|
||||
workspaces.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)));
|
||||
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
|
||||
komorebi_client::Layout::Default(layout) => {
|
||||
KomorebiLayout::Default(*layout)
|
||||
}
|
||||
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
self.workspaces = workspaces;
|
||||
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
|
||||
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
|
||||
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
|
||||
};
|
||||
|
||||
if !*monitor.workspaces()[focused_workspace_idx].tile() {
|
||||
self.layout = KomorebiLayout::Floating;
|
||||
}
|
||||
|
||||
if notification.state.is_paused {
|
||||
self.layout = KomorebiLayout::Paused;
|
||||
}
|
||||
|
||||
if let Some(container) = monitor.workspaces()[focused_workspace_idx].monocle_container()
|
||||
{
|
||||
self.focused_container_information = (
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.title().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
container.focused_window_idx(),
|
||||
);
|
||||
} else if let Some(container) =
|
||||
monitor.workspaces()[focused_workspace_idx].focused_container()
|
||||
{
|
||||
self.focused_container_information = (
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.title().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
container.focused_window_idx(),
|
||||
);
|
||||
} else {
|
||||
self.focused_container_information.0.clear();
|
||||
self.focused_container_information.1.clear();
|
||||
self.focused_container_information.2 = 0;
|
||||
self.focused_container_information =
|
||||
(&monitor.workspaces()[focused_workspace_idx]).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationStateContainerInformation {
|
||||
pub titles: Vec<String>,
|
||||
pub icons: Vec<Option<RgbaImage>>,
|
||||
pub focused_window_idx: usize,
|
||||
}
|
||||
|
||||
impl From<&Workspace> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Workspace) -> Self {
|
||||
let mut container_info = Self::EMPTY;
|
||||
|
||||
if let Some(container) = value.monocle_container() {
|
||||
container_info = container.into();
|
||||
} else if let Some(container) = value.focused_container() {
|
||||
container_info = container.into();
|
||||
}
|
||||
|
||||
for floating_window in value.floating_windows() {
|
||||
if floating_window.is_focused() {
|
||||
container_info = floating_window.into();
|
||||
}
|
||||
}
|
||||
|
||||
container_info
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Container> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Container) -> Self {
|
||||
Self {
|
||||
titles: value
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.title().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
icons: value
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
|
||||
.collect::<Vec<_>>(),
|
||||
focused_window_idx: value.focused_window_idx(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Window> for KomorebiNotificationStateContainerInformation {
|
||||
fn from(value: &Window) -> Self {
|
||||
Self {
|
||||
titles: vec![value.title().unwrap_or_default()],
|
||||
icons: vec![windows_icons::get_icon_by_process_id(value.process_id())],
|
||||
focused_window_idx: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KomorebiNotificationStateContainerInformation {
|
||||
pub const EMPTY: Self = Self {
|
||||
titles: vec![],
|
||||
icons: vec![],
|
||||
focused_window_idx: 0,
|
||||
};
|
||||
}
|
||||
|
||||
311
komorebi-bar/src/komorebi_layout.rs
Normal file
311
komorebi-bar/src/komorebi_layout.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
use crate::config::DisplayFormat;
|
||||
use crate::komorebi::KomorebiLayoutConfig;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use eframe::egui::vec2;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Rounding;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::TextStyle;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::SocketMessage;
|
||||
use schemars::JsonSchema;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde_json::from_str;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum KomorebiLayout {
|
||||
Default(komorebi_client::DefaultLayout),
|
||||
Monocle,
|
||||
Floating,
|
||||
Paused,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: String = String::deserialize(deserializer)?;
|
||||
|
||||
// Attempt to deserialize the string as a DefaultLayout
|
||||
if let Ok(default_layout) =
|
||||
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
|
||||
{
|
||||
return Ok(KomorebiLayout::Default(default_layout));
|
||||
}
|
||||
|
||||
// Handle other cases
|
||||
match s.as_str() {
|
||||
"Monocle" => Ok(KomorebiLayout::Monocle),
|
||||
"Floating" => Ok(KomorebiLayout::Floating),
|
||||
"Paused" => Ok(KomorebiLayout::Paused),
|
||||
"Custom" => Ok(KomorebiLayout::Custom),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for KomorebiLayout {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
|
||||
KomorebiLayout::Monocle => write!(f, "Monocle"),
|
||||
KomorebiLayout::Floating => write!(f, "Floating"),
|
||||
KomorebiLayout::Paused => write!(f, "Paused"),
|
||||
KomorebiLayout::Custom => write!(f, "Custom"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KomorebiLayout {
|
||||
fn is_default(&mut self) -> bool {
|
||||
matches!(self, KomorebiLayout::Default(_))
|
||||
}
|
||||
|
||||
fn on_click(
|
||||
&mut self,
|
||||
show_options: &bool,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: Option<usize>,
|
||||
) -> bool {
|
||||
if self.is_default() {
|
||||
!show_options
|
||||
} else {
|
||||
self.on_click_option(monitor_idx, workspace_idx);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
|
||||
match self {
|
||||
KomorebiLayout::Default(option) => {
|
||||
if let Some(ws_idx) = workspace_idx {
|
||||
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
||||
monitor_idx,
|
||||
ws_idx,
|
||||
*option,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
||||
}
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Monocle => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() {
|
||||
tracing::error!("could not send message to komorebi: ToggleMonocle");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Floating => {
|
||||
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() {
|
||||
tracing::error!("could not send message to komorebi: ToggleTiling");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Paused => {
|
||||
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
|
||||
tracing::error!("could not send message to komorebi: TogglePause");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Custom => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_icon(&mut self, font_id: FontId, ctx: &Context, ui: &mut Ui) {
|
||||
// paint custom icons for the layout
|
||||
let size = Vec2::splat(font_id.size);
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::hover());
|
||||
let color = ctx.style().visuals.selection.stroke.color;
|
||||
let stroke = Stroke::new(1.0, color);
|
||||
let mut rect = response.rect;
|
||||
let rounding = Rounding::same(rect.width() * 0.1);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke);
|
||||
|
||||
match self {
|
||||
KomorebiLayout::Default(layout) => match layout {
|
||||
komorebi_client::DefaultLayout::BSP => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::Columns => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::Rows => {
|
||||
painter.line_segment([c - vec2(r, r / 2.0), c + vec2(r, -r / 2.0)], stroke);
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(r, -r / 2.0), c + vec2(r, r / 2.0)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::VerticalStack => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(r, 0.0), c], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::HorizontalStack => {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c, c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
komorebi_client::DefaultLayout::Grid => {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
},
|
||||
KomorebiLayout::Monocle => {}
|
||||
KomorebiLayout::Floating => {
|
||||
let mut rect_left = response.rect;
|
||||
rect_left.set_width(rect.width() * 0.5);
|
||||
rect_left.set_height(rect.height() * 0.5);
|
||||
let mut rect_right = rect_left;
|
||||
rect_left = rect_left.translate(Vec2::new(
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
));
|
||||
rect_right = rect_right.translate(Vec2::new(
|
||||
rect.width() * 0.35 + stroke.width,
|
||||
rect.width() * 0.35 + stroke.width,
|
||||
));
|
||||
painter.rect_filled(rect_left, rounding, color);
|
||||
painter.rect_stroke(rect_right, rounding, stroke);
|
||||
}
|
||||
KomorebiLayout::Paused => {
|
||||
let mut rect_left = response.rect;
|
||||
rect_left.set_width(rect.width() * 0.25);
|
||||
rect_left.set_height(rect.height() * 0.8);
|
||||
let mut rect_right = rect_left;
|
||||
rect_left = rect_left.translate(Vec2::new(
|
||||
rect.width() * 0.2 + stroke.width,
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
));
|
||||
rect_right = rect_right.translate(Vec2::new(
|
||||
rect.width() * 0.55 + stroke.width,
|
||||
rect.width() * 0.1 + stroke.width,
|
||||
));
|
||||
painter.rect_filled(rect_left, rounding, color);
|
||||
painter.rect_filled(rect_right, rounding, color);
|
||||
}
|
||||
KomorebiLayout::Custom => {
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c + vec2(0.0, r / 2.0), c + vec2(r, r / 2.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r / 3.0), c - vec2(r, r / 3.0)], stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
ui: &mut Ui,
|
||||
render_config: &mut RenderConfig,
|
||||
layout_config: &KomorebiLayoutConfig,
|
||||
workspace_idx: Option<usize>,
|
||||
) {
|
||||
let monitor_idx = render_config.monitor_idx;
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
|
||||
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);
|
||||
|
||||
if !self.is_default() {
|
||||
show_options = false;
|
||||
}
|
||||
|
||||
render_config.apply_on_widget(false, ui, |ui| {
|
||||
let layout_frame = SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
|
||||
self.show_icon(font_id.clone(), ctx, ui);
|
||||
}
|
||||
|
||||
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
|
||||
ui.add(Label::new(self.to_string()).selectable(false));
|
||||
}
|
||||
})
|
||||
.on_hover_text(self.to_string());
|
||||
|
||||
if layout_frame.clicked() {
|
||||
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
|
||||
}
|
||||
|
||||
if show_options {
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
Frame::none().show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||
.selectable(false),
|
||||
);
|
||||
|
||||
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::HorizontalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
||||
//KomorebiLayout::Custom,
|
||||
KomorebiLayout::Monocle,
|
||||
KomorebiLayout::Floating,
|
||||
KomorebiLayout::Paused,
|
||||
]);
|
||||
|
||||
for layout_option in &mut layout_options {
|
||||
if SelectableFrame::new(self == layout_option)
|
||||
.show(ui, |ui| layout_option.show_icon(font_id.clone(), ctx, ui))
|
||||
.on_hover_text(match layout_option {
|
||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
||||
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
||||
KomorebiLayout::Custom => "Custom".to_string(),
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
||||
show_options = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
RenderConfig::store_show_komorebi_layout_options(show_options);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
mod bar;
|
||||
mod battery;
|
||||
mod config;
|
||||
mod cpu;
|
||||
mod date;
|
||||
mod komorebi;
|
||||
mod komorebi_layout;
|
||||
mod media;
|
||||
mod memory;
|
||||
mod network;
|
||||
mod render;
|
||||
mod selected_frame;
|
||||
mod storage;
|
||||
mod time;
|
||||
mod ui;
|
||||
@@ -14,29 +18,40 @@ mod widget;
|
||||
use crate::bar::Komobar;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::Position;
|
||||
use atomic_float::AtomicF32;
|
||||
use crate::config::PositionConfig;
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::bail;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::SubscribeOptions;
|
||||
use schemars::gen::SchemaSettings;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::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::SetProcessDpiAwarenessContext;
|
||||
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
|
||||
pub static WIDGET_SPACING: f32 = 10.0;
|
||||
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
|
||||
pub static DPI: AtomicF32 = AtomicF32::new(1.0);
|
||||
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;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
@@ -53,36 +68,41 @@ struct Opts {
|
||||
/// Write an example komorebi.bar.json to disk
|
||||
#[clap(long)]
|
||||
quickstart: bool,
|
||||
/// Print a list of aliases that can be renamed to canonical variants
|
||||
#[clap(long)]
|
||||
#[clap(hide = true)]
|
||||
aliases: bool,
|
||||
}
|
||||
|
||||
macro_rules! as_ptr {
|
||||
($value:expr) => {
|
||||
$value as *mut core::ffi::c_void
|
||||
};
|
||||
}
|
||||
|
||||
pub fn dpi_for_monitor(hmonitor: isize) -> color_eyre::Result<f32> {
|
||||
use windows::Win32::Graphics::Gdi::HMONITOR;
|
||||
use windows::Win32::UI::HiDpi::GetDpiForMonitor;
|
||||
use windows::Win32::UI::HiDpi::MDT_EFFECTIVE_DPI;
|
||||
|
||||
let mut dpi_x = u32::default();
|
||||
let mut dpi_y = u32::default();
|
||||
|
||||
extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
unsafe {
|
||||
match GetDpiForMonitor(
|
||||
HMONITOR(as_ptr!(hmonitor)),
|
||||
MDT_EFFECTIVE_DPI,
|
||||
std::ptr::addr_of_mut!(dpi_x),
|
||||
std::ptr::addr_of_mut!(dpi_y),
|
||||
) {
|
||||
Ok(_) => {}
|
||||
Err(error) => bail!(error),
|
||||
let mut process_id = 0;
|
||||
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
|
||||
|
||||
if process_id == GetCurrentProcessId() {
|
||||
*(lparam.0 as *mut HWND) = hwnd;
|
||||
BOOL::from(false) // Stop enumeration
|
||||
} else {
|
||||
BOOL::from(true) // Continue enumeration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
Ok(dpi_y as f32 / 96.0)
|
||||
fn process_hwnd() -> Option<isize> {
|
||||
unsafe {
|
||||
let mut hwnd = HWND::default();
|
||||
let _ = EnumThreadWindows(
|
||||
GetCurrentThreadId(),
|
||||
Some(enum_window),
|
||||
LPARAM(&mut hwnd as *mut HWND as isize),
|
||||
);
|
||||
|
||||
if hwnd.0 as isize == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(hwnd.0 as isize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
@@ -166,7 +186,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
Option::from,
|
||||
);
|
||||
|
||||
let config = match config_path {
|
||||
let mut config = match config_path {
|
||||
None => {
|
||||
let komorebi_bar_json =
|
||||
include_str!("../../docs/komorebi.bar.example.json").to_string();
|
||||
@@ -180,10 +200,12 @@ fn main() -> color_eyre::Result<()> {
|
||||
KomobarConfig::read(&default_config_path)?
|
||||
}
|
||||
Some(ref config) => {
|
||||
tracing::info!(
|
||||
"found configuration file: {}",
|
||||
config.as_path().to_string_lossy()
|
||||
);
|
||||
if !opts.aliases {
|
||||
tracing::info!(
|
||||
"found configuration file: {}",
|
||||
config.as_path().to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
KomobarConfig::read(config)?
|
||||
}
|
||||
@@ -191,46 +213,67 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
let config_path = config_path.unwrap_or(default_config_path);
|
||||
|
||||
if opts.aliases {
|
||||
KomobarConfig::aliases(&std::fs::read_to_string(&config_path)?);
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let state = serde_json::from_str::<komorebi_client::State>(&komorebi_client::send_query(
|
||||
&SocketMessage::State,
|
||||
)?)?;
|
||||
|
||||
let dpi = dpi_for_monitor(state.monitors.elements()[config.monitor.index].id())?;
|
||||
DPI.store(dpi, Ordering::SeqCst);
|
||||
MONITOR_RIGHT.store(
|
||||
state.monitors.elements()[config.monitor.index].size().right,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
let mut viewport_builder = ViewportBuilder::default()
|
||||
.with_decorations(false)
|
||||
// .with_transparent(config.transparent)
|
||||
.with_taskbar(false)
|
||||
.with_position(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().left as f32 / dpi,
|
||||
y: state.monitors.elements()[config.monitor.index].size().top as f32 / dpi,
|
||||
})
|
||||
.with_inner_size({
|
||||
Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32 / dpi,
|
||||
y: 50.0 / dpi,
|
||||
}
|
||||
});
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[config.monitor.index].size().top,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
if let Some(viewport) = &config.viewport {
|
||||
if let Some(mut position) = &viewport.position {
|
||||
position.x /= dpi;
|
||||
position.y /= dpi;
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[config.monitor.index].size().left,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
let b = viewport_builder.clone();
|
||||
viewport_builder = b.with_position(position);
|
||||
MONITOR_INDEX.store(config.monitor.index, Ordering::SeqCst);
|
||||
|
||||
match config.position {
|
||||
None => {
|
||||
config.position = Some(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().left as f32,
|
||||
y: state.monitors.elements()[config.monitor.index].size().top as f32,
|
||||
}),
|
||||
end: Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32,
|
||||
y: 50.0,
|
||||
}),
|
||||
})
|
||||
}
|
||||
Some(ref mut position) => {
|
||||
if position.start.is_none() {
|
||||
position.start = Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().left as f32,
|
||||
y: state.monitors.elements()[config.monitor.index].size().top as f32,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(mut inner_size) = &viewport.inner_size {
|
||||
inner_size.x /= dpi;
|
||||
inner_size.y /= dpi;
|
||||
|
||||
let b = viewport_builder.clone();
|
||||
viewport_builder = b.with_inner_size(inner_size);
|
||||
if position.end.is_none() {
|
||||
position.end = Some(Position {
|
||||
x: state.monitors.elements()[config.monitor.index].size().right as f32,
|
||||
y: 50.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let viewport_builder = ViewportBuilder::default()
|
||||
.with_decorations(false)
|
||||
.with_transparent(true)
|
||||
.with_taskbar(false);
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: viewport_builder,
|
||||
..Default::default()
|
||||
@@ -291,7 +334,9 @@ fn main() -> color_eyre::Result<()> {
|
||||
std::thread::spawn(move || {
|
||||
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
|
||||
|
||||
let listener = komorebi_client::subscribe(&subscriber_name)
|
||||
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
|
||||
filter_state_changes: true,
|
||||
})
|
||||
.expect("could not subscribe to komorebi notifications");
|
||||
|
||||
tracing::info!("subscribed to komorebi notifications: \"{}\"", subscriber_name);
|
||||
@@ -333,18 +378,21 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
match String::from_utf8(buffer) {
|
||||
Ok(notification_string) => {
|
||||
if let Ok(notification) =
|
||||
serde_json::from_str::<komorebi_client::Notification>(
|
||||
¬ification_string,
|
||||
)
|
||||
{
|
||||
tracing::debug!("received notification from komorebi");
|
||||
match serde_json::from_str::<komorebi_client::Notification>(
|
||||
¬ification_string,
|
||||
) {
|
||||
Ok(notification) => {
|
||||
tracing::debug!("received notification from komorebi");
|
||||
|
||||
if let Err(error) = tx_gui.send(notification) {
|
||||
tracing::error!("could not send komorebi notification update to gui: {error}")
|
||||
if let Err(error) = tx_gui.send(notification) {
|
||||
tracing::error!("could not send komorebi notification update to gui thread: {error}")
|
||||
}
|
||||
|
||||
ctx_komorebi.request_repaint();
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("could not deserialize komorebi notification: {error}");
|
||||
}
|
||||
|
||||
ctx_komorebi.request_repaint();
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::render::RenderConfig;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
@@ -78,7 +78,7 @@ impl Media {
|
||||
}
|
||||
|
||||
impl BarWidget for Media {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
@@ -102,26 +102,26 @@ impl BarWidget for Media {
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
if custom_ui
|
||||
.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click())
|
||||
.truncate(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.toggle();
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
@@ -23,6 +24,8 @@ pub struct MemoryConfig {
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<MemoryConfig> for Memory {
|
||||
@@ -36,6 +39,7 @@ impl From<MemoryConfig> for Memory {
|
||||
enable: value.enable,
|
||||
system,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
@@ -45,6 +49,7 @@ pub struct Memory {
|
||||
pub enable: bool,
|
||||
system: System,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
@@ -58,12 +63,17 @@ impl Memory {
|
||||
|
||||
let used = self.system.used_memory();
|
||||
let total = self.system.total_memory();
|
||||
format!("RAM: {}%", (used * 100) / total)
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Memory {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
@@ -75,7 +85,12 @@ impl BarWidget for Memory {
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
egui_phosphor::regular::MEMORY.to_string(),
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::MEMORY.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
@@ -87,22 +102,23 @@ impl BarWidget for Memory {
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
@@ -30,85 +31,44 @@ pub struct NetworkConfig {
|
||||
pub network_activity_fill_characters: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<NetworkConfig> for Network {
|
||||
fn from(value: NetworkConfig) -> Self {
|
||||
let mut last_state_data = vec![];
|
||||
let mut last_state_transmitted = vec![];
|
||||
|
||||
let mut networks_total_data_transmitted = Networks::new_with_refreshed_list();
|
||||
let mut networks_network_activity = Networks::new_with_refreshed_list();
|
||||
|
||||
let mut default_interface = String::new();
|
||||
|
||||
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(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),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
networks_total_data_transmitted,
|
||||
networks_network_activity,
|
||||
default_interface,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
show_total_data_transmitted: value.show_total_data_transmitted,
|
||||
show_network_activity: value.show_network_activity,
|
||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
||||
default_interface: String::new(),
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
show_total_activity: value.show_total_data_transmitted,
|
||||
show_activity: value.show_network_activity,
|
||||
network_activity_fill_characters: value
|
||||
.network_activity_fill_characters
|
||||
.unwrap_or_default(),
|
||||
last_state_total_data_transmitted: last_state_data,
|
||||
last_state_network_activity: last_state_transmitted,
|
||||
last_updated_total_data_transmitted: Instant::now(),
|
||||
last_updated_network_activity: Instant::now(),
|
||||
last_state_total_activity: vec![],
|
||||
last_state_activity: vec![],
|
||||
last_updated_network_activity: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Network {
|
||||
pub enable: bool,
|
||||
pub show_total_data_transmitted: bool,
|
||||
pub show_network_activity: bool,
|
||||
networks_total_data_transmitted: Networks,
|
||||
pub show_total_activity: bool,
|
||||
pub show_activity: bool,
|
||||
networks_network_activity: Networks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
default_interface: String,
|
||||
last_state_total_data_transmitted: Vec<String>,
|
||||
last_state_network_activity: Vec<String>,
|
||||
last_updated_total_data_transmitted: Instant,
|
||||
last_state_total_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
last_updated_network_activity: Instant,
|
||||
network_activity_fill_characters: usize,
|
||||
}
|
||||
@@ -122,29 +82,44 @@ impl Network {
|
||||
}
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> Vec<String> {
|
||||
let mut outputs = self.last_state_network_activity.clone();
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let mut activity = self.last_state_activity.clone();
|
||||
let mut total_activity = self.last_state_total_activity.clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if self.show_network_activity
|
||||
&& now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
if now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
outputs.clear();
|
||||
activity.clear();
|
||||
total_activity.clear();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
if self.show_network_activity {
|
||||
self.networks_network_activity.refresh();
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
outputs.push(format!(
|
||||
"{} {: >width$}/s {} {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.received(), self.data_refresh_interval),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.transmitted(), self.data_refresh_interval),
|
||||
width = self.network_activity_fill_characters,
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
|
||||
self.networks_network_activity.refresh();
|
||||
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if self.show_activity {
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
Self::to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval,
|
||||
),
|
||||
Self::to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
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),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -152,67 +127,147 @@ impl Network {
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_network_activity.clone_from(&outputs);
|
||||
self.last_state_activity.clone_from(&activity);
|
||||
self.last_state_total_activity.clone_from(&total_activity);
|
||||
self.last_updated_network_activity = now;
|
||||
}
|
||||
|
||||
outputs
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
fn total_data_transmitted(&mut self) -> Vec<String> {
|
||||
let mut outputs = self.last_state_total_data_transmitted.clone();
|
||||
let now = Instant::now();
|
||||
fn reading_to_label(&self, ctx: &Context, reading: NetworkReading) -> Label {
|
||||
let (text_down, text_up) = match self.label_prefix {
|
||||
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"{: >width$}/s | ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"{: >width$}/s",
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("{} | ", reading.received_text),
|
||||
reading.transmitted_text,
|
||||
),
|
||||
},
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
|
||||
NetworkReadingFormat::Speed => (
|
||||
format!(
|
||||
"DOWN: {: >width$}/s | ",
|
||||
reading.received_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
format!(
|
||||
"UP: {: >width$}/s",
|
||||
reading.transmitted_text,
|
||||
width = self.network_activity_fill_characters
|
||||
),
|
||||
),
|
||||
NetworkReadingFormat::Total => (
|
||||
format!("\u{2211}DOWN: {}/s | ", reading.received_text),
|
||||
format!("\u{2211}UP: {}/s", reading.transmitted_text),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
if self.show_total_data_transmitted
|
||||
&& now.duration_since(self.last_updated_total_data_transmitted)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
{
|
||||
outputs.clear();
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
if self.show_total_data_transmitted {
|
||||
self.networks_total_data_transmitted.refresh();
|
||||
let icon_format =
|
||||
TextFormat::simple(font_id.clone(), ctx.style().visuals.selection.stroke.color);
|
||||
let text_format = TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color());
|
||||
|
||||
for (interface_name, data) in &self.networks_total_data_transmitted {
|
||||
if friendly_name.eq(interface_name) {
|
||||
outputs.push(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),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
// icon
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
|
||||
}
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
icon_format.font_id.clone(),
|
||||
icon_format.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
self.last_state_total_data_transmitted.clone_from(&outputs);
|
||||
self.last_updated_total_data_transmitted = now;
|
||||
// text
|
||||
layout_job.append(
|
||||
&text_down,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
// icon
|
||||
layout_job.append(
|
||||
&match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::ARROW_FAT_UP.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
0.0,
|
||||
icon_format.clone(),
|
||||
);
|
||||
|
||||
// text
|
||||
layout_job.append(
|
||||
&text_up,
|
||||
ctx.style().spacing.item_spacing.x,
|
||||
text_format.clone(),
|
||||
);
|
||||
|
||||
Label::new(layout_job).selectable(false)
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
|
||||
let input = input_in_bytes as f32 / timespan_in_s as f32;
|
||||
let mut magnitude = input.log(1024f32) as u32;
|
||||
|
||||
// let the base unit be KiB
|
||||
if magnitude < 1 {
|
||||
magnitude = 1;
|
||||
}
|
||||
|
||||
outputs
|
||||
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
|
||||
let result = input / ((1u64) << (magnitude * 10)) as f32;
|
||||
|
||||
match base {
|
||||
Some(DataUnit::B) => format!("{result:.1} B"),
|
||||
Some(unit) => format!("{result:.1} {unit}iB"),
|
||||
None => String::from("Unknown data unit"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidget for Network {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
if self.show_total_data_transmitted {
|
||||
for output in self.total_data_transmitted() {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
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 {
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if self.show_network_activity {
|
||||
for output in self.network_activity() {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
if self.show_activity {
|
||||
for reading in activity {
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(self.reading_to_label(ctx, reading));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if self.enable {
|
||||
@@ -227,33 +282,65 @@ impl BarWidget for Network {
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
egui_phosphor::regular::WIFI_HIGH.to_string(),
|
||||
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)
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
#[derive(Clone)]
|
||||
enum NetworkReadingFormat {
|
||||
Speed = 0,
|
||||
Total = 1,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NetworkReading {
|
||||
pub format: NetworkReadingFormat,
|
||||
pub received_text: String,
|
||||
pub transmitted_text: String,
|
||||
}
|
||||
|
||||
impl NetworkReading {
|
||||
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
|
||||
NetworkReading {
|
||||
format,
|
||||
received_text: received,
|
||||
transmitted_text: transmitted,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,22 +363,3 @@ impl fmt::Display for DataUnit {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
|
||||
let input = input_in_bytes as f32 / timespan_in_s as f32;
|
||||
let mut magnitude = input.log(1024f32) as u32;
|
||||
|
||||
// let the base unit be KiB
|
||||
if magnitude < 1 {
|
||||
magnitude = 1;
|
||||
}
|
||||
|
||||
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
|
||||
let result = input / ((1u64) << (magnitude * 10)) as f32;
|
||||
|
||||
match base {
|
||||
Some(DataUnit::B) => format!("{result:.1} B"),
|
||||
Some(unit) => format!("{result:.1} {unit}iB"),
|
||||
None => String::from("Unknown data unit"),
|
||||
}
|
||||
}
|
||||
|
||||
283
komorebi-bar/src/render.rs
Normal file
283
komorebi-bar/src/render.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::KomobarConfig;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::InnerResponse;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Rounding;
|
||||
use eframe::egui::Shadow;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
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(Copy, 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,
|
||||
}
|
||||
|
||||
pub trait RenderExt {
|
||||
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig;
|
||||
}
|
||||
|
||||
impl RenderExt for &KomobarConfig {
|
||||
fn new_renderconfig(&self, background_color: Color32) -> RenderConfig {
|
||||
RenderConfig {
|
||||
monitor_idx: self.monitor.index,
|
||||
spacing: self.widget_spacing.unwrap_or(10.0),
|
||||
grouping: self.grouping.unwrap_or(Grouping::None),
|
||||
background_color,
|
||||
alignment: None,
|
||||
more_inner_margin: false,
|
||||
applied_on_widget: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_on_bar<R>(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.alignment = None;
|
||||
|
||||
if let Grouping::Bar(config) = self.grouping {
|
||||
return self.define_group(None, config, ui, add_contents);
|
||||
}
|
||||
|
||||
Self::fallback_group(ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn apply_on_alignment<R>(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.alignment = None;
|
||||
|
||||
if let Grouping::Alignment(config) = self.grouping {
|
||||
return self.define_group(None, config, ui, add_contents);
|
||||
}
|
||||
|
||||
Self::fallback_group(ui, add_contents)
|
||||
}
|
||||
|
||||
pub fn apply_on_widget<R>(
|
||||
&mut self,
|
||||
more_inner_margin: bool,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
self.more_inner_margin = more_inner_margin;
|
||||
let outer_margin = self.widget_outer_margin(ui);
|
||||
|
||||
if let Grouping::Widget(config) = self.grouping {
|
||||
return self.define_group(Some(outer_margin), config, ui, add_contents);
|
||||
}
|
||||
|
||||
self.fallback_widget_group(Some(outer_margin), ui, add_contents)
|
||||
}
|
||||
|
||||
fn fallback_group<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||
InnerResponse {
|
||||
inner: add_contents(ui),
|
||||
response: ui.response().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_widget_group<R>(
|
||||
&mut self,
|
||||
outer_margin: Option<Margin>,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
Frame::none()
|
||||
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
|
||||
.inner_margin(match self.more_inner_margin {
|
||||
true => Margin::symmetric(5.0, 0.0),
|
||||
false => Margin::same(0.0),
|
||||
})
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
|
||||
fn define_group<R>(
|
||||
&mut self,
|
||||
outer_margin: Option<Margin>,
|
||||
config: GroupingConfig,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
Frame::group(ui.style_mut())
|
||||
.outer_margin(outer_margin.unwrap_or(Margin::ZERO))
|
||||
.inner_margin(match self.more_inner_margin {
|
||||
true => Margin::symmetric(8.0, 3.0),
|
||||
false => Margin::symmetric(3.0, 3.0),
|
||||
})
|
||||
.stroke(ui.style().visuals.widgets.noninteractive.bg_stroke)
|
||||
.rounding(match config.rounding {
|
||||
Some(rounding) => rounding.into(),
|
||||
None => ui.style().visuals.widgets.noninteractive.rounding,
|
||||
})
|
||||
.fill(
|
||||
self.background_color
|
||||
.try_apply_alpha(config.transparency_alpha),
|
||||
)
|
||||
.shadow(match config.style {
|
||||
Some(style) => match style {
|
||||
// new styles can be added if needed here
|
||||
GroupingStyle::Default => Shadow::NONE,
|
||||
GroupingStyle::DefaultWithShadow => Shadow {
|
||||
blur: 4.0,
|
||||
offset: Vec2::new(1.0, 1.0),
|
||||
spread: 3.0,
|
||||
color: Color32::BLACK.try_apply_alpha(config.transparency_alpha),
|
||||
},
|
||||
},
|
||||
None => Shadow::NONE,
|
||||
})
|
||||
.show(ui, add_contents)
|
||||
}
|
||||
|
||||
fn widget_outer_margin(&mut self, ui: &mut Ui) -> Margin {
|
||||
let spacing = if self.applied_on_widget {
|
||||
// Remove the default item spacing from the margin
|
||||
self.spacing - ui.spacing().item_spacing.x
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if !self.applied_on_widget {
|
||||
self.applied_on_widget = true;
|
||||
}
|
||||
|
||||
Margin {
|
||||
left: match self.alignment {
|
||||
Some(align) => match align {
|
||||
Alignment::Left => spacing,
|
||||
Alignment::Center => spacing,
|
||||
Alignment::Right => 0.0,
|
||||
},
|
||||
None => 0.0,
|
||||
},
|
||||
right: match self.alignment {
|
||||
Some(align) => match align {
|
||||
Alignment::Left => 0.0,
|
||||
Alignment::Center => 0.0,
|
||||
Alignment::Right => spacing,
|
||||
},
|
||||
None => 0.0,
|
||||
},
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GroupingConfig {
|
||||
/// Styles for the grouping
|
||||
pub style: Option<GroupingStyle>,
|
||||
/// Alpha value for the color transparency [[0-255]] (default: 200)
|
||||
pub transparency_alpha: Option<u8>,
|
||||
/// Rounding values for the 4 corners. Can be a single or 4 values.
|
||||
pub rounding: Option<RoundingConfig>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum GroupingStyle {
|
||||
#[serde(alias = "CtByte")]
|
||||
Default,
|
||||
/// A black shadow is added under the default group
|
||||
#[serde(alias = "CtByteWithShadow")]
|
||||
DefaultWithShadow,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum RoundingConfig {
|
||||
/// All 4 corners are the same
|
||||
Same(f32),
|
||||
/// All 4 corners are custom. Order: NW, NE, SW, SE
|
||||
Individual([f32; 4]),
|
||||
}
|
||||
|
||||
impl From<RoundingConfig> for Rounding {
|
||||
fn from(value: RoundingConfig) -> Self {
|
||||
match value {
|
||||
RoundingConfig::Same(value) => Rounding::same(value),
|
||||
RoundingConfig::Individual(values) => Self {
|
||||
nw: values[0],
|
||||
ne: values[1],
|
||||
sw: values[2],
|
||||
se: values[3],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Color32Ext {
|
||||
fn try_apply_alpha(self, transparency_alpha: Option<u8>) -> Self;
|
||||
}
|
||||
|
||||
impl Color32Ext for Color32 {
|
||||
/// Tries to apply the alpha value to the Color32
|
||||
fn try_apply_alpha(self, transparency_alpha: Option<u8>) -> Self {
|
||||
if let Some(alpha) = transparency_alpha {
|
||||
return Color32::from_rgba_unmultiplied(self.r(), self.g(), self.b(), alpha);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
55
komorebi-bar/src/selected_frame.rs
Normal file
55
komorebi-bar/src/selected_frame.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::Response;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Ui;
|
||||
|
||||
/// Same as SelectableLabel, but supports all content
|
||||
pub struct SelectableFrame {
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl SelectableFrame {
|
||||
pub fn new(selected: bool) -> Self {
|
||||
Self { selected }
|
||||
}
|
||||
|
||||
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
|
||||
let Self { selected } = self;
|
||||
|
||||
Frame::none()
|
||||
.show(ui, |ui| {
|
||||
let response = ui.interact(ui.max_rect(), ui.unique_id(), Sense::click());
|
||||
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
let inner_margin = Margin::symmetric(
|
||||
ui.style().spacing.button_padding.x,
|
||||
ui.style().spacing.button_padding.y,
|
||||
);
|
||||
|
||||
if selected
|
||||
|| response.hovered()
|
||||
|| response.highlighted()
|
||||
|| response.has_focus()
|
||||
{
|
||||
let visuals = ui.style().interact_selectable(&response, selected);
|
||||
|
||||
Frame::none()
|
||||
.stroke(visuals.bg_stroke)
|
||||
.rounding(visuals.rounding)
|
||||
.fill(visuals.bg_fill)
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
} else {
|
||||
Frame::none()
|
||||
.inner_margin(inner_margin)
|
||||
.show(ui, add_contents);
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
})
|
||||
.inner
|
||||
.on_hover_cursor(eframe::egui::CursorIcon::PointingHand)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
@@ -22,6 +23,8 @@ pub struct StorageConfig {
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<StorageConfig> for Storage {
|
||||
@@ -30,6 +33,7 @@ impl From<StorageConfig> for Storage {
|
||||
enable: value.enable,
|
||||
disks: Disks::new_with_refreshed_list(),
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
@@ -39,6 +43,7 @@ pub struct Storage {
|
||||
pub enable: bool,
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
@@ -58,11 +63,12 @@ impl Storage {
|
||||
let available = disk.available_space();
|
||||
let used = total - available;
|
||||
|
||||
disks.push(format!(
|
||||
"{} {}%",
|
||||
mount.to_string_lossy(),
|
||||
(used * 100) / total
|
||||
))
|
||||
disks.push(match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
|
||||
})
|
||||
}
|
||||
|
||||
disks.sort();
|
||||
@@ -73,7 +79,7 @@ impl Storage {
|
||||
}
|
||||
|
||||
impl BarWidget for Storage {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
@@ -84,7 +90,12 @@ impl BarWidget for Storage {
|
||||
|
||||
for output in self.output() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
egui_phosphor::regular::HARD_DRIVES.to_string(),
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::HARD_DRIVES.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
@@ -96,27 +107,27 @@ impl BarWidget for Storage {
|
||||
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widget::BarWidget;
|
||||
use crate::WIDGET_SPACING;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::FontId;
|
||||
@@ -18,6 +19,8 @@ pub struct TimeConfig {
|
||||
pub enable: bool,
|
||||
/// Set the Time format
|
||||
pub format: TimeFormat,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
}
|
||||
|
||||
impl From<TimeConfig> for Time {
|
||||
@@ -25,6 +28,7 @@ impl From<TimeConfig> for Time {
|
||||
Self {
|
||||
enable: value.enable,
|
||||
format: value.format,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +65,7 @@ impl TimeFormat {
|
||||
pub struct Time {
|
||||
pub enable: bool,
|
||||
pub format: TimeFormat,
|
||||
label_prefix: LabelPrefix,
|
||||
}
|
||||
|
||||
impl Time {
|
||||
@@ -72,9 +77,9 @@ impl Time {
|
||||
}
|
||||
|
||||
impl BarWidget for Time {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
let output = self.output();
|
||||
let mut output = self.output();
|
||||
if !output.is_empty() {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
@@ -84,31 +89,40 @@ impl BarWidget for Time {
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
egui_phosphor::regular::CLOCK.to_string(),
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
egui_phosphor::regular::CLOCK.to_string()
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Text => String::new(),
|
||||
},
|
||||
font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
100.0,
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
output.insert_str(0, "TIME: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.toggle()
|
||||
}
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(layout_job)
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
self.format.toggle()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::battery::Battery;
|
||||
use crate::battery::BatteryConfig;
|
||||
use crate::cpu::Cpu;
|
||||
use crate::cpu::CpuConfig;
|
||||
use crate::date::Date;
|
||||
use crate::date::DateConfig;
|
||||
use crate::komorebi::Komorebi;
|
||||
@@ -10,6 +12,7 @@ use crate::memory::Memory;
|
||||
use crate::memory::MemoryConfig;
|
||||
use crate::network::Network;
|
||||
use crate::network::NetworkConfig;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::storage::Storage;
|
||||
use crate::storage::StorageConfig;
|
||||
use crate::time::Time;
|
||||
@@ -21,12 +24,13 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub trait BarWidget {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui);
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum WidgetConfig {
|
||||
Battery(BatteryConfig),
|
||||
Cpu(CpuConfig),
|
||||
Date(DateConfig),
|
||||
Komorebi(KomorebiConfig),
|
||||
Media(MediaConfig),
|
||||
@@ -40,6 +44,7 @@ impl WidgetConfig {
|
||||
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
|
||||
match self {
|
||||
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
|
||||
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
|
||||
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
|
||||
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
|
||||
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.30"
|
||||
version = "0.1.31"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::animation::prefix::AnimationPrefix;
|
||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||
pub use komorebi::colour::Colour;
|
||||
pub use komorebi::colour::Rgb;
|
||||
pub use komorebi::config_generation::ApplicationConfiguration;
|
||||
@@ -44,6 +46,7 @@ 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;
|
||||
@@ -52,6 +55,7 @@ use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::Shutdown;
|
||||
use std::time::Duration;
|
||||
pub use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
@@ -60,6 +64,7 @@ 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())
|
||||
}
|
||||
|
||||
@@ -67,6 +72,8 @@ 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)?;
|
||||
|
||||
@@ -96,3 +103,29 @@ pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
pub fn subscribe_with_options(
|
||||
name: &str,
|
||||
options: SubscribeOptions,
|
||||
) -> std::io::Result<UnixListener> {
|
||||
let socket = DATA_DIR.join(name);
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(()) => {}
|
||||
Err(error) => match error.kind() {
|
||||
std::io::ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
|
||||
send_message(&SocketMessage::AddSubscriberSocketWithOptions(
|
||||
name.to_string(),
|
||||
options,
|
||||
))?;
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.30"
|
||||
version = "0.1.31"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -27,7 +27,6 @@ fn main() {
|
||||
viewport: ViewportBuilder::default()
|
||||
.with_always_on_top()
|
||||
.with_inner_size([320.0, 500.0]),
|
||||
follow_system_theme: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -234,7 +233,8 @@ extern "system" fn enum_window(
|
||||
|
||||
fn json_view_ui(ui: &mut egui::Ui, code: &str) {
|
||||
let language = "json";
|
||||
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
let theme =
|
||||
egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), &ui.ctx().style());
|
||||
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.30"
|
||||
version = "0.1.31"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "a2c48f45782c5604bf5482d3873021a9fe45ea1a" }
|
||||
catppuccin-egui = { version = "5.1", default-features = false, features = ["egui28"] }
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "c11fbe2a3a4681485c5065b899a4c4d85fad3b04" }
|
||||
#catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "f579847bf2f552b144361d5a78ed8cf360b55cbb" }
|
||||
catppuccin-egui = { version = "5", default-features = false, features = ["egui29"] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_variant = "0.1"
|
||||
strum = "0.26"
|
||||
@@ -4,10 +4,12 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub use base16_egui_themes::Base16;
|
||||
pub use catppuccin_egui;
|
||||
pub use eframe::egui::Color32;
|
||||
use serde_variant::to_variant_name;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -24,6 +26,28 @@ pub enum Theme {
|
||||
},
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn variant_names(&self) -> Vec<String> {
|
||||
match self {
|
||||
Theme::Catppuccin { .. } => {
|
||||
vec![
|
||||
"Frappe".to_string(),
|
||||
"Latte".to_string(),
|
||||
"Macchiato".to_string(),
|
||||
"Mocha".to_string(),
|
||||
]
|
||||
}
|
||||
Theme::Base16 { .. } => Base16::iter()
|
||||
.map(|variant| {
|
||||
to_variant_name(&variant)
|
||||
.expect("could not convert to variant name")
|
||||
.to_string()
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Base16Value {
|
||||
Base00,
|
||||
@@ -124,35 +148,39 @@ pub enum CatppuccinValue {
|
||||
Crust,
|
||||
}
|
||||
|
||||
pub fn color32_compat(rgba: [u8; 4]) -> Color32 {
|
||||
Color32::from_rgba_unmultiplied(rgba[0], rgba[1], rgba[2], rgba[3])
|
||||
}
|
||||
|
||||
impl CatppuccinValue {
|
||||
pub fn color32(&self, theme: catppuccin_egui::Theme) -> Color32 {
|
||||
match self {
|
||||
CatppuccinValue::Rosewater => theme.rosewater,
|
||||
CatppuccinValue::Flamingo => theme.flamingo,
|
||||
CatppuccinValue::Pink => theme.pink,
|
||||
CatppuccinValue::Mauve => theme.mauve,
|
||||
CatppuccinValue::Red => theme.red,
|
||||
CatppuccinValue::Maroon => theme.maroon,
|
||||
CatppuccinValue::Peach => theme.peach,
|
||||
CatppuccinValue::Yellow => theme.yellow,
|
||||
CatppuccinValue::Green => theme.green,
|
||||
CatppuccinValue::Teal => theme.teal,
|
||||
CatppuccinValue::Sky => theme.sky,
|
||||
CatppuccinValue::Sapphire => theme.sapphire,
|
||||
CatppuccinValue::Blue => theme.blue,
|
||||
CatppuccinValue::Lavender => theme.lavender,
|
||||
CatppuccinValue::Text => theme.text,
|
||||
CatppuccinValue::Subtext1 => theme.subtext1,
|
||||
CatppuccinValue::Subtext0 => theme.subtext0,
|
||||
CatppuccinValue::Overlay2 => theme.overlay2,
|
||||
CatppuccinValue::Overlay1 => theme.overlay1,
|
||||
CatppuccinValue::Overlay0 => theme.overlay0,
|
||||
CatppuccinValue::Surface2 => theme.surface2,
|
||||
CatppuccinValue::Surface1 => theme.surface1,
|
||||
CatppuccinValue::Surface0 => theme.surface0,
|
||||
CatppuccinValue::Base => theme.base,
|
||||
CatppuccinValue::Mantle => theme.mantle,
|
||||
CatppuccinValue::Crust => theme.crust,
|
||||
CatppuccinValue::Rosewater => color32_compat(theme.rosewater.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Flamingo => color32_compat(theme.flamingo.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Pink => color32_compat(theme.pink.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Mauve => color32_compat(theme.mauve.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Red => color32_compat(theme.red.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Maroon => color32_compat(theme.maroon.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Peach => color32_compat(theme.peach.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Yellow => color32_compat(theme.yellow.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Green => color32_compat(theme.green.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Teal => color32_compat(theme.teal.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Sky => color32_compat(theme.sky.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Sapphire => color32_compat(theme.sapphire.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Blue => color32_compat(theme.blue.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Lavender => color32_compat(theme.lavender.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Text => color32_compat(theme.text.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Subtext1 => color32_compat(theme.subtext1.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Subtext0 => color32_compat(theme.subtext0.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Overlay2 => color32_compat(theme.overlay2.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Overlay1 => color32_compat(theme.overlay1.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Overlay0 => color32_compat(theme.overlay0.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Surface2 => color32_compat(theme.surface2.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Surface1 => color32_compat(theme.surface1.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Surface0 => color32_compat(theme.surface0.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Base => color32_compat(theme.base.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Mantle => color32_compat(theme.mantle.to_srgba_unmultiplied()),
|
||||
CatppuccinValue::Crust => color32_compat(theme.crust.to_srgba_unmultiplied()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.30"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
version = "0.1.31"
|
||||
description = "A tiling window manager for Windows"
|
||||
categories = ["tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -44,7 +42,6 @@ tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
uds_windows = { workspace = true }
|
||||
which = { workspace = true }
|
||||
widestring = "1"
|
||||
win32-display-data = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
|
||||
115
komorebi/src/animation/animation_manager.rs
Normal file
115
komorebi/src/animation/animation_manager.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::prefix::AnimationPrefix;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct AnimationState {
|
||||
pub in_progress: bool,
|
||||
pub cancel_idx_counter: usize,
|
||||
pub pending_cancel_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AnimationManager {
|
||||
animations: HashMap<String, AnimationState>,
|
||||
}
|
||||
|
||||
impl Default for AnimationManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
animations: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self, animation_key: &str) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(animation_key) {
|
||||
animation_state.pending_cancel_count > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_progress(&self, animation_key: &str) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(animation_key) {
|
||||
animation_state.in_progress
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_cancel(&mut self, animation_key: &str) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.pending_cancel_count += 1;
|
||||
animation_state.cancel_idx_counter += 1;
|
||||
|
||||
// return cancel idx
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn latest_cancel_idx(&mut self, animation_key: &str) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_cancel(&mut self, animation_key: &str) {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.pending_cancel_count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, animation_key: &str) {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.in_progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, animation_key: &str) {
|
||||
if let Entry::Vacant(e) = self.animations.entry(animation_key.to_string()) {
|
||||
e.insert(AnimationState {
|
||||
in_progress: true,
|
||||
cancel_idx_counter: 0,
|
||||
pending_cancel_count: 0,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.in_progress = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&mut self, animation_key: &str) {
|
||||
if let Some(animation_state) = self.animations.get_mut(animation_key) {
|
||||
animation_state.in_progress = false;
|
||||
|
||||
if animation_state.pending_cancel_count == 0 {
|
||||
self.animations.remove(animation_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count_in_progress(&self, animation_key_prefix: AnimationPrefix) -> usize {
|
||||
self.animations
|
||||
.keys()
|
||||
.filter(|key| key.starts_with(animation_key_prefix.to_string().as_str()))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.animations.len()
|
||||
}
|
||||
}
|
||||
122
komorebi/src/animation/engine.rs
Normal file
122
komorebi/src/animation/engine.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::RenderDispatcher;
|
||||
use super::ANIMATION_DURATION_GLOBAL;
|
||||
use super::ANIMATION_FPS;
|
||||
use super::ANIMATION_MANAGER;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct AnimationEngine;
|
||||
|
||||
impl AnimationEngine {
|
||||
pub fn wait_for_all_animations() {
|
||||
let max_duration = Duration::from_secs(20);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().count() > 0 {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
break;
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(
|
||||
ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the animation needs to continue
|
||||
pub fn cancel(animation_key: &str) -> bool {
|
||||
// should be more than 0
|
||||
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(animation_key);
|
||||
let max_duration = Duration::from_secs(5);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().in_progress(animation_key) {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
ANIMATION_MANAGER.lock().end(animation_key);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(250 / 2));
|
||||
}
|
||||
|
||||
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(animation_key);
|
||||
|
||||
ANIMATION_MANAGER.lock().end_cancel(animation_key);
|
||||
|
||||
latest_cancel_idx == cancel_idx
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
render_dispatcher: (impl RenderDispatcher + Send + 'static),
|
||||
duration: Duration,
|
||||
) -> Result<()> {
|
||||
std::thread::spawn(move || {
|
||||
let animation_key = render_dispatcher.get_animation_key();
|
||||
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
|
||||
let should_animate = Self::cancel(animation_key.as_str());
|
||||
|
||||
if !should_animate {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
render_dispatcher.pre_render()?;
|
||||
|
||||
ANIMATION_MANAGER.lock().start(animation_key.as_str());
|
||||
|
||||
let target_frame_time =
|
||||
Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
|
||||
let mut progress = 0.0;
|
||||
let animation_start = Instant::now();
|
||||
|
||||
// start animation
|
||||
while progress < 1.0 {
|
||||
// check if animation is cancelled
|
||||
if ANIMATION_MANAGER
|
||||
.lock()
|
||||
.is_cancelled(animation_key.as_str())
|
||||
{
|
||||
// cancel animation
|
||||
ANIMATION_MANAGER.lock().cancel(animation_key.as_str());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame_start = Instant::now();
|
||||
// calculate progress
|
||||
progress =
|
||||
animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
|
||||
render_dispatcher.render(progress).ok();
|
||||
|
||||
// sleep until next frame
|
||||
let frame_time_elapsed = frame_start.elapsed();
|
||||
|
||||
if frame_time_elapsed < target_frame_time {
|
||||
std::thread::sleep(target_frame_time - frame_time_elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().end(animation_key.as_str());
|
||||
|
||||
// limit progress to 1.0 if animation took longer
|
||||
if progress != 1.0 {
|
||||
progress = 1.0;
|
||||
|
||||
// process animation for 1.0 to set target position
|
||||
render_dispatcher.render(progress).ok();
|
||||
}
|
||||
|
||||
render_dispatcher.post_render()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
42
komorebi/src/animation/lerp.rs
Normal file
42
komorebi/src/animation/lerp.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use crate::core::Rect;
|
||||
use crate::AnimationStyle;
|
||||
|
||||
use super::style::apply_ease_func;
|
||||
|
||||
pub trait Lerp<T = Self> {
|
||||
fn lerp(self, end: T, time: f64, style: AnimationStyle) -> T;
|
||||
}
|
||||
|
||||
impl Lerp for i32 {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn lerp(self, end: i32, time: f64, style: AnimationStyle) -> i32 {
|
||||
let time = apply_ease_func(time, style);
|
||||
|
||||
f64::from(end - self).mul_add(time, f64::from(self)).round() as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for f64 {
|
||||
fn lerp(self, end: f64, time: f64, style: AnimationStyle) -> f64 {
|
||||
let time = apply_ease_func(time, style);
|
||||
|
||||
(end - self).mul_add(time, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for u8 {
|
||||
fn lerp(self, end: u8, time: f64, style: AnimationStyle) -> u8 {
|
||||
(self as f64).lerp(end as f64, time, style) as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for Rect {
|
||||
fn lerp(self, end: Rect, time: f64, style: AnimationStyle) -> Rect {
|
||||
Rect {
|
||||
left: self.left.lerp(end.left, time, style),
|
||||
top: self.top.lerp(end.top, time, style),
|
||||
right: self.right.lerp(end.right, time, style),
|
||||
bottom: self.bottom.lerp(end.bottom, time, style),
|
||||
}
|
||||
}
|
||||
}
|
||||
54
komorebi/src/animation/mod.rs
Normal file
54
komorebi/src/animation/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::animation::animation_manager::AnimationManager;
|
||||
use crate::core::animation::AnimationStyle;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use prefix::AnimationPrefix;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
pub use engine::AnimationEngine;
|
||||
pub mod animation_manager;
|
||||
pub mod engine;
|
||||
pub mod lerp;
|
||||
pub mod prefix;
|
||||
pub mod render_dispatcher;
|
||||
pub use render_dispatcher::RenderDispatcher;
|
||||
pub mod style;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum PerAnimationPrefixConfig<T> {
|
||||
Prefix(HashMap<AnimationPrefix, T>),
|
||||
Global(T),
|
||||
}
|
||||
|
||||
pub const DEFAULT_ANIMATION_ENABLED: bool = false;
|
||||
pub const DEFAULT_ANIMATION_STYLE: AnimationStyle = AnimationStyle::Linear;
|
||||
pub const DEFAULT_ANIMATION_DURATION: u64 = 250;
|
||||
pub const DEFAULT_ANIMATION_FPS: u64 = 60;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
|
||||
Arc::new(Mutex::new(AnimationManager::new()));
|
||||
pub static ref ANIMATION_STYLE_GLOBAL: Arc<Mutex<AnimationStyle>> =
|
||||
Arc::new(Mutex::new(DEFAULT_ANIMATION_STYLE));
|
||||
pub static ref ANIMATION_ENABLED_GLOBAL: Arc<AtomicBool> =
|
||||
Arc::new(AtomicBool::new(DEFAULT_ANIMATION_ENABLED));
|
||||
pub static ref ANIMATION_DURATION_GLOBAL: Arc<AtomicU64> =
|
||||
Arc::new(AtomicU64::new(DEFAULT_ANIMATION_DURATION));
|
||||
pub static ref ANIMATION_STYLE_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, AnimationStyle>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref ANIMATION_ENABLED_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, bool>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref ANIMATION_DURATION_PER_ANIMATION: Arc<Mutex<HashMap<AnimationPrefix, u64>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
}
|
||||
|
||||
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(DEFAULT_ANIMATION_FPS);
|
||||
31
komorebi/src/animation/prefix.rs
Normal file
31
komorebi/src/animation/prefix.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use clap::ValueEnum;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AnimationPrefix {
|
||||
Movement,
|
||||
Transparency,
|
||||
}
|
||||
|
||||
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
|
||||
format!("{}:{}", prefix, key)
|
||||
}
|
||||
8
komorebi/src/animation/render_dispatcher.rs
Normal file
8
komorebi/src/animation/render_dispatcher.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use color_eyre::Result;
|
||||
|
||||
pub trait RenderDispatcher {
|
||||
fn get_animation_key(&self) -> String;
|
||||
fn pre_render(&self) -> Result<()>;
|
||||
fn render(&self, delta: f64) -> Result<()>;
|
||||
fn post_render(&self) -> Result<()>;
|
||||
}
|
||||
@@ -1,22 +1,6 @@
|
||||
use crate::core::AnimationStyle;
|
||||
use crate::core::Rect;
|
||||
use color_eyre::Result;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::f64::consts::PI;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_MANAGER;
|
||||
use crate::ANIMATION_STYLE;
|
||||
|
||||
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(60);
|
||||
|
||||
pub trait Ease {
|
||||
fn evaluate(t: f64) -> f64;
|
||||
@@ -370,9 +354,8 @@ impl Ease for EaseInOutBounce {
|
||||
}
|
||||
}
|
||||
}
|
||||
fn apply_ease_func(t: f64) -> f64 {
|
||||
let style = *ANIMATION_STYLE.lock();
|
||||
|
||||
pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
match style {
|
||||
AnimationStyle::Linear => Linear::evaluate(t),
|
||||
AnimationStyle::EaseInSine => EaseInSine::evaluate(t),
|
||||
@@ -406,112 +389,3 @@ fn apply_ease_func(t: f64) -> f64 {
|
||||
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Animation {
|
||||
pub hwnd: isize,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new(hwnd: isize) -> Self {
|
||||
Self { hwnd }
|
||||
}
|
||||
|
||||
/// Returns true if the animation needs to continue
|
||||
pub fn cancel(&mut self) -> bool {
|
||||
if !ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// should be more than 0
|
||||
let cancel_idx = ANIMATION_MANAGER.lock().init_cancel(self.hwnd);
|
||||
let max_duration = Duration::from_secs(1);
|
||||
let spent_duration = Instant::now();
|
||||
|
||||
while ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
if spent_duration.elapsed() >= max_duration {
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(
|
||||
ANIMATION_DURATION.load(Ordering::SeqCst) / 2,
|
||||
));
|
||||
}
|
||||
|
||||
let latest_cancel_idx = ANIMATION_MANAGER.lock().latest_cancel_idx(self.hwnd);
|
||||
|
||||
ANIMATION_MANAGER.lock().end_cancel(self.hwnd);
|
||||
|
||||
latest_cancel_idx == cancel_idx
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn lerp(start: i32, end: i32, t: f64) -> i32 {
|
||||
let time = apply_ease_func(t);
|
||||
f64::from(end - start)
|
||||
.mul_add(time, f64::from(start))
|
||||
.round() as i32
|
||||
}
|
||||
|
||||
pub fn lerp_rect(start_rect: &Rect, end_rect: &Rect, t: f64) -> Rect {
|
||||
Rect {
|
||||
left: Self::lerp(start_rect.left, end_rect.left, t),
|
||||
top: Self::lerp(start_rect.top, end_rect.top, t),
|
||||
right: Self::lerp(start_rect.right, end_rect.right, t),
|
||||
bottom: Self::lerp(start_rect.bottom, end_rect.bottom, t),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
&mut self,
|
||||
duration: Duration,
|
||||
mut render_callback: impl FnMut(f64) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
if ANIMATION_MANAGER.lock().in_progress(self.hwnd) {
|
||||
let should_animate = self.cancel();
|
||||
|
||||
if !should_animate {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().start(self.hwnd);
|
||||
|
||||
let target_frame_time = Duration::from_millis(1000 / ANIMATION_FPS.load(Ordering::Relaxed));
|
||||
let mut progress = 0.0;
|
||||
let animation_start = Instant::now();
|
||||
|
||||
// start animation
|
||||
while progress < 1.0 {
|
||||
// check if animation is cancelled
|
||||
if ANIMATION_MANAGER.lock().is_cancelled(self.hwnd) {
|
||||
// cancel animation
|
||||
ANIMATION_MANAGER.lock().cancel(self.hwnd);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame_start = Instant::now();
|
||||
// calculate progress
|
||||
progress = animation_start.elapsed().as_millis() as f64 / duration.as_millis() as f64;
|
||||
render_callback(progress).ok();
|
||||
|
||||
// sleep until next frame
|
||||
let frame_time_elapsed = frame_start.elapsed();
|
||||
|
||||
if frame_time_elapsed < target_frame_time {
|
||||
std::thread::sleep(target_frame_time - frame_time_elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
ANIMATION_MANAGER.lock().end(self.hwnd);
|
||||
|
||||
// limit progress to 1.0 if animation took longer
|
||||
if progress > 1.0 {
|
||||
progress = 1.0;
|
||||
}
|
||||
|
||||
// process animation for 1.0 to set target position
|
||||
render_callback(progress)
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub static ANIMATIONS_IN_PROGRESS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct AnimationState {
|
||||
pub in_progress: bool,
|
||||
pub cancel_idx_counter: usize,
|
||||
pub pending_cancel_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AnimationManager {
|
||||
animations: HashMap<isize, AnimationState>,
|
||||
}
|
||||
|
||||
impl Default for AnimationManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
animations: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self, hwnd: isize) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(&hwnd) {
|
||||
animation_state.pending_cancel_count > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_progress(&self, hwnd: isize) -> bool {
|
||||
if let Some(animation_state) = self.animations.get(&hwnd) {
|
||||
animation_state.in_progress
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_cancel(&mut self, hwnd: isize) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.pending_cancel_count += 1;
|
||||
animation_state.cancel_idx_counter += 1;
|
||||
|
||||
// return cancel idx
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn latest_cancel_idx(&mut self, hwnd: isize) -> usize {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.cancel_idx_counter
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_cancel(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.pending_cancel_count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, hwnd: isize) {
|
||||
if let Entry::Vacant(e) = self.animations.entry(hwnd) {
|
||||
e.insert(AnimationState {
|
||||
in_progress: true,
|
||||
cancel_idx_counter: 0,
|
||||
pending_cancel_count: 0,
|
||||
});
|
||||
|
||||
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&mut self, hwnd: isize) {
|
||||
if let Some(animation_state) = self.animations.get_mut(&hwnd) {
|
||||
animation_state.in_progress = false;
|
||||
|
||||
if animation_state.pending_cancel_count == 0 {
|
||||
self.animations.remove(&hwnd);
|
||||
ANIMATIONS_IN_PROGRESS.store(self.animations.len(), Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,47 +3,86 @@ use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::FOCUS_STATE;
|
||||
use crate::border_manager::RENDER_TARGETS;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use windows::core::PCWSTR;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Foundation::Numerics::Matrix3x2;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::FALSE;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::TRUE;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Gdi::BeginPaint;
|
||||
use windows::Win32::Graphics::Gdi::CreatePen;
|
||||
use windows::Win32::Graphics::Gdi::DeleteObject;
|
||||
use windows::Win32::Graphics::Gdi::EndPaint;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_HWND_RENDER_TARGET_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_PRESENT_OPTIONS_IMMEDIATELY;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
|
||||
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
|
||||
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
|
||||
use windows::Win32::Graphics::Gdi::CreateRectRgn;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::RoundRect;
|
||||
use windows::Win32::Graphics::Gdi::SelectObject;
|
||||
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
|
||||
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
use windows::Win32::Graphics::Gdi::ValidateRect;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows_core::PCWSTR;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
static RENDER_FACTORY: LazyLock<ID2D1Factory> = unsafe {
|
||||
LazyLock::new(|| {
|
||||
D2D1CreateFactory::<ID2D1Factory>(D2D1_FACTORY_TYPE_MULTI_THREADED, None)
|
||||
.expect("creating RENDER_FACTORY failed")
|
||||
})
|
||||
};
|
||||
|
||||
static BRUSH_PROPERTIES: LazyLock<D2D1_BRUSH_PROPERTIES> =
|
||||
LazyLock::new(|| D2D1_BRUSH_PROPERTIES {
|
||||
opacity: 1.0,
|
||||
transform: Matrix3x2::identity(),
|
||||
});
|
||||
|
||||
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||
@@ -58,14 +97,36 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
true.into()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Border {
|
||||
pub hwnd: isize,
|
||||
pub render_target: OnceLock<ID2D1HwndRenderTarget>,
|
||||
pub tracking_hwnd: isize,
|
||||
pub window_rect: Rect,
|
||||
pub window_kind: WindowKind,
|
||||
pub style: BorderStyle,
|
||||
pub width: i32,
|
||||
pub offset: i32,
|
||||
pub brush_properties: D2D1_BRUSH_PROPERTIES,
|
||||
pub rounded_rect: D2D1_ROUNDED_RECT,
|
||||
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
|
||||
}
|
||||
|
||||
impl From<isize> for Border {
|
||||
fn from(value: isize) -> Self {
|
||||
Self { hwnd: value }
|
||||
Self {
|
||||
hwnd: value,
|
||||
render_target: OnceLock::new(),
|
||||
tracking_hwnd: 0,
|
||||
window_rect: Rect::default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
style: STYLE.load(),
|
||||
width: BORDER_WIDTH.load(Ordering::Relaxed),
|
||||
offset: BORDER_OFFSET.load(Ordering::Relaxed),
|
||||
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
|
||||
rounded_rect: D2D1_ROUNDED_RECT::default(),
|
||||
brushes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +135,7 @@ impl Border {
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
}
|
||||
|
||||
pub fn create(id: &str) -> color_eyre::Result<Self> {
|
||||
pub fn create(id: &str, tracking_hwnd: isize) -> color_eyre::Result<Self> {
|
||||
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
|
||||
let class_name = PCWSTR(name.as_ptr());
|
||||
|
||||
@@ -83,7 +144,6 @@ impl Border {
|
||||
let window_class = WNDCLASSW {
|
||||
hInstance: h_module.into(),
|
||||
lpszClassName: class_name,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(Self::callback),
|
||||
hbrBackground: WindowsApi::create_solid_brush(0),
|
||||
..Default::default()
|
||||
@@ -91,12 +151,30 @@ impl Border {
|
||||
|
||||
let _ = WindowsApi::register_class_w(&window_class);
|
||||
|
||||
let (hwnd_sender, hwnd_receiver) = mpsc::channel();
|
||||
let (border_sender, border_receiver) = mpsc::channel();
|
||||
|
||||
let instance = h_module.0 as isize;
|
||||
std::thread::spawn(move || -> color_eyre::Result<()> {
|
||||
let hwnd = WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance)?;
|
||||
hwnd_sender.send(hwnd)?;
|
||||
let mut border = Self {
|
||||
hwnd: 0,
|
||||
render_target: OnceLock::new(),
|
||||
tracking_hwnd,
|
||||
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
|
||||
window_kind: WindowKind::Unfocused,
|
||||
style: STYLE.load(),
|
||||
width: BORDER_WIDTH.load(Ordering::Relaxed),
|
||||
offset: BORDER_OFFSET.load(Ordering::Relaxed),
|
||||
brush_properties: Default::default(),
|
||||
rounded_rect: Default::default(),
|
||||
brushes: HashMap::new(),
|
||||
};
|
||||
|
||||
let border_pointer = std::ptr::addr_of!(border);
|
||||
let hwnd =
|
||||
WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance, border_pointer)?;
|
||||
|
||||
border.hwnd = hwnd;
|
||||
border_sender.send(border_pointer as isize)?;
|
||||
|
||||
let mut msg: MSG = MSG::default();
|
||||
|
||||
@@ -110,42 +188,117 @@ impl Border {
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
hwnd: hwnd_receiver.recv()?,
|
||||
})
|
||||
let border_ref = border_receiver.recv()?;
|
||||
let border = unsafe { &mut *(border_ref as *mut Border) };
|
||||
|
||||
// I have literally no idea, apparently this is to get rid of the black pixels
|
||||
// around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys
|
||||
unsafe {
|
||||
let pos: i32 = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8;
|
||||
let hrgn = CreateRectRgn(pos, 0, pos + 1, 1);
|
||||
let mut bh: DWM_BLURBEHIND = Default::default();
|
||||
if !hrgn.is_invalid() {
|
||||
bh = DWM_BLURBEHIND {
|
||||
dwFlags: DWM_BB_ENABLE | DWM_BB_BLURREGION,
|
||||
fEnable: TRUE,
|
||||
hRgnBlur: hrgn,
|
||||
fTransitionOnMaximized: FALSE,
|
||||
};
|
||||
}
|
||||
|
||||
let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh);
|
||||
}
|
||||
|
||||
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
|
||||
hwnd: HWND(windows_api::as_ptr!(border.hwnd)),
|
||||
pixelSize: Default::default(),
|
||||
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
|
||||
};
|
||||
|
||||
let render_target_properties = D2D1_RENDER_TARGET_PROPERTIES {
|
||||
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
||||
pixelFormat: D2D1_PIXEL_FORMAT {
|
||||
format: DXGI_FORMAT_UNKNOWN,
|
||||
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
|
||||
},
|
||||
dpiX: 96.0,
|
||||
dpiY: 96.0,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match unsafe {
|
||||
RENDER_FACTORY
|
||||
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
|
||||
} {
|
||||
Ok(render_target) => unsafe {
|
||||
border.brush_properties = *BRUSH_PROPERTIES.deref();
|
||||
for window_kind in [
|
||||
WindowKind::Single,
|
||||
WindowKind::Stack,
|
||||
WindowKind::Monocle,
|
||||
WindowKind::Unfocused,
|
||||
WindowKind::Floating,
|
||||
] {
|
||||
let color = window_kind_colour(window_kind);
|
||||
let color = D2D1_COLOR_F {
|
||||
r: ((color & 0xFF) as f32) / 255.0,
|
||||
g: (((color >> 8) & 0xFF) as f32) / 255.0,
|
||||
b: (((color >> 16) & 0xFF) as f32) / 255.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
if let Ok(brush) =
|
||||
render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties))
|
||||
{
|
||||
border.brushes.insert(window_kind, brush);
|
||||
}
|
||||
}
|
||||
|
||||
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||
|
||||
if border.render_target.set(render_target.clone()).is_err() {
|
||||
return Err(anyhow!("could not store border render target"));
|
||||
}
|
||||
|
||||
border.rounded_rect = {
|
||||
let radius = 8.0 + border.width as f32 / 2.0;
|
||||
D2D1_ROUNDED_RECT {
|
||||
rect: Default::default(),
|
||||
radiusX: radius,
|
||||
radiusY: radius,
|
||||
}
|
||||
};
|
||||
|
||||
let mut render_targets = RENDER_TARGETS.lock();
|
||||
render_targets.insert(border.hwnd, render_target);
|
||||
Ok(border.clone())
|
||||
},
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
let mut render_targets = RENDER_TARGETS.lock();
|
||||
render_targets.remove(&self.hwnd);
|
||||
WindowsApi::close_window(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn update(&self, rect: &Rect, mut should_invalidate: bool) -> color_eyre::Result<()> {
|
||||
// Make adjustments to the border
|
||||
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
|
||||
let mut rect = *rect;
|
||||
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst));
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
|
||||
rect.add_margin(self.width);
|
||||
rect.add_padding(-self.offset);
|
||||
|
||||
// Update the position of the border if required
|
||||
if !WindowsApi::window_rect(self.hwnd)?.eq(&rect) {
|
||||
WindowsApi::set_border_pos(self.hwnd, &rect, Z_ORDER.load().into())?;
|
||||
should_invalidate = true;
|
||||
}
|
||||
|
||||
// Invalidate the rect to trigger the callback to update colours etc.
|
||||
if should_invalidate {
|
||||
self.invalidate();
|
||||
}
|
||||
WindowsApi::set_border_pos(self.hwnd, &rect, reference_hwnd)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// this triggers WM_PAINT in the callback below
|
||||
pub fn invalidate(&self) {
|
||||
let _ = unsafe { InvalidateRect(self.hwnd(), None, false) };
|
||||
}
|
||||
@@ -158,75 +311,196 @@ impl Border {
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_PAINT => {
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
WM_CREATE => {
|
||||
let mut border_pointer: *mut Border =
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
// With the rect that we set in Self::update
|
||||
match WindowsApi::window_rect(window.0 as isize) {
|
||||
Ok(rect) => {
|
||||
// Grab the focus kind for this border
|
||||
let window_kind = {
|
||||
FOCUS_STATE
|
||||
.lock()
|
||||
.get(&(window.0 as isize))
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused)
|
||||
if border_pointer.is_null() {
|
||||
let create_struct: *mut CREATESTRUCTW = lparam.0 as *mut _;
|
||||
border_pointer = (*create_struct).lpCreateParams as *mut _;
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, border_pointer as _);
|
||||
}
|
||||
|
||||
LRESULT(0)
|
||||
}
|
||||
EVENT_OBJECT_DESTROY => {
|
||||
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
// we don't actually want to destroy the window here, just hide it for quicker
|
||||
// visual feedback to the user; the actual destruction will be handled by the
|
||||
// core border manager loop
|
||||
WindowsApi::hide_window(window.0 as isize);
|
||||
LRESULT(0)
|
||||
}
|
||||
EVENT_OBJECT_LOCATIONCHANGE => {
|
||||
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let reference_hwnd = lparam.0;
|
||||
|
||||
let old_rect = (*border_pointer).window_rect;
|
||||
let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default();
|
||||
|
||||
(*border_pointer).window_rect = rect;
|
||||
|
||||
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if !rect.is_same_size_as(&old_rect) {
|
||||
if let Some(render_target) = (*border_pointer).render_target.get() {
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
// Set up the brush to draw the border
|
||||
let hpen = CreatePen(
|
||||
PS_SOLID | PS_INSIDEFRAME,
|
||||
BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
COLORREF(window_kind_colour(window_kind)),
|
||||
);
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let hbrush = WindowsApi::create_solid_brush(0);
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Draw the border
|
||||
SelectObject(hdc, hpen);
|
||||
SelectObject(hdc, hbrush);
|
||||
// TODO(raggi): this is approximately the correct curvature for
|
||||
// the top left of a Windows 11 window (DWMWCP_DEFAULT), but
|
||||
// often the bottom right has a different shape. Furthermore if
|
||||
// the window was made with DWMWCP_ROUNDSMALL then this is the
|
||||
// wrong size. In the future we should read the DWM properties
|
||||
// of windows and attempt to match appropriately.
|
||||
match STYLE.load() {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
// TODO: error handling
|
||||
let _ =
|
||||
RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
} else {
|
||||
// TODO: error handling
|
||||
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
BorderStyle::Rounded => {
|
||||
// TODO: error handling
|
||||
let _ = RoundRect(hdc, 0, 0, rect.right, rect.bottom, 20, 20);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
// TODO: error handling
|
||||
let _ = Rectangle(hdc, 0, 0, rect.right, rect.bottom);
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hpen);
|
||||
// TODO: error handling
|
||||
let _ = DeleteObject(hbrush);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("could not get border rect: {}", error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: error handling
|
||||
let _ = EndPaint(window, &ps);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_PAINT => {
|
||||
if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
|
||||
let border_pointer: *mut Border =
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
|
||||
|
||||
if border_pointer.is_null() {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
if let Some(render_target) = (*border_pointer).render_target.get() {
|
||||
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
||||
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
||||
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
// Get window kind and color
|
||||
|
||||
(*border_pointer).window_kind = FOCUS_STATE
|
||||
.lock()
|
||||
.get(&(window.0 as isize))
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused);
|
||||
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
(*border_pointer).style = STYLE.load();
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = ValidateRect(window, None);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod border;
|
||||
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::WindowKind;
|
||||
@@ -12,7 +11,7 @@ use crate::Rgb;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
use border::Border;
|
||||
pub use border::Border;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
@@ -30,15 +29,14 @@ use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
|
||||
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
|
||||
|
||||
pub static BORDER_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub static BORDER_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref Z_ORDER: AtomicCell<ZOrder> = AtomicCell::new(ZOrder::Bottom);
|
||||
pub static ref STYLE: AtomicCell<BorderStyle> = AtomicCell::new(BorderStyle::System);
|
||||
pub static ref IMPLEMENTATION: AtomicCell<BorderImplementation> =
|
||||
AtomicCell::new(BorderImplementation::Komorebi);
|
||||
@@ -49,15 +47,20 @@ lazy_static! {
|
||||
pub static ref MONOCLE: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(255, 51, 153))));
|
||||
pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(0, 165, 66))));
|
||||
pub static ref FLOATING: AtomicU32 =
|
||||
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(245, 245, 165))));
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
|
||||
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
|
||||
static ref WINDOWS_BORDERS: Mutex<HashMap<isize, Border>> = Mutex::new(HashMap::new());
|
||||
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
|
||||
static ref RENDER_TARGETS: Mutex<HashMap<isize, ID2D1HwndRenderTarget>> =
|
||||
Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub struct Notification;
|
||||
pub struct Notification(pub Option<isize>);
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
@@ -73,8 +76,12 @@ fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification() {
|
||||
if event_tx().try_send(Notification).is_err() {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -87,12 +94,13 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
);
|
||||
|
||||
for (_, border) in borders.iter() {
|
||||
border.destroy()?;
|
||||
let _ = border.destroy();
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
BORDERS_MONITORS.lock().clear();
|
||||
FOCUS_STATE.lock().clear();
|
||||
RENDER_TARGETS.lock().clear();
|
||||
|
||||
let mut remaining_hwnds = vec![];
|
||||
|
||||
@@ -105,7 +113,7 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
tracing::info!("purging unknown borders: {:?}", remaining_hwnds);
|
||||
|
||||
for hwnd in remaining_hwnds {
|
||||
Border::from(hwnd).destroy()?;
|
||||
let _ = Border::from(hwnd).destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,10 +122,11 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
|
||||
|
||||
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
||||
match focus_kind {
|
||||
WindowKind::Unfocused => UNFOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Stack => STACK.load(Ordering::SeqCst),
|
||||
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
|
||||
WindowKind::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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,21 +146,30 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
let receiver = event_rx();
|
||||
event_tx().send(Notification)?;
|
||||
event_tx().send(Notification(None))?;
|
||||
|
||||
let mut previous_snapshot = Ring::default();
|
||||
let mut previous_pending_move_op = None;
|
||||
let mut previous_is_paused = false;
|
||||
let mut previous_notification: Option<Notification> = None;
|
||||
|
||||
'receiver: for _ in receiver {
|
||||
'receiver: for notification in receiver {
|
||||
// Check the wm state every time we receive a notification
|
||||
let state = wm.lock();
|
||||
let is_paused = state.is_paused;
|
||||
let focused_monitor_idx = state.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
state.monitors.elements()[focused_monitor_idx].focused_workspace_idx();
|
||||
let monitors = state.monitors.clone();
|
||||
let pending_move_op = state.pending_move_op;
|
||||
let pending_move_op = *state.pending_move_op;
|
||||
let floating_window_hwnds = state.monitors.elements()[focused_monitor_idx].workspaces()
|
||||
[focused_workspace_idx]
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
drop(state);
|
||||
|
||||
match IMPLEMENTATION.load() {
|
||||
@@ -220,6 +238,21 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
// when we switch focus to a floating window
|
||||
if !should_process_notification
|
||||
&& floating_window_hwnds.contains(¬ification.0.unwrap_or_default())
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
if let Some(ref previous) = previous_notification {
|
||||
if previous.0.unwrap_or_default() != notification.0.unwrap_or_default() {
|
||||
should_process_notification = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
@@ -227,11 +260,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
let mut borders = BORDER_STATE.lock();
|
||||
let mut borders_monitors = BORDERS_MONITORS.lock();
|
||||
let mut windows_borders = WINDOWS_BORDERS.lock();
|
||||
|
||||
// If borders are disabled
|
||||
if !BORDER_ENABLED.load_consume()
|
||||
// Or if they are temporarily disabled
|
||||
|| BORDER_TEMPORARILY_DISABLED.load(Ordering::SeqCst)
|
||||
// Or if the wm is paused
|
||||
|| is_paused
|
||||
// Or if we are handling an alt-tab across workspaces
|
||||
@@ -243,6 +275,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
borders.clear();
|
||||
windows_borders.clear();
|
||||
|
||||
previous_is_paused = is_paused;
|
||||
continue 'receiver;
|
||||
@@ -275,7 +308,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let border = match borders.entry(monocle.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(monocle.id()) {
|
||||
if let Ok(border) = Border::create(
|
||||
monocle.id(),
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd,
|
||||
) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
@@ -284,6 +320,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
};
|
||||
|
||||
borders_monitors.insert(monocle.id().clone(), monitor_idx);
|
||||
windows_borders.insert(
|
||||
monocle.focused_window().cloned().unwrap_or_default().hwnd,
|
||||
border.clone(),
|
||||
);
|
||||
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
@@ -297,11 +337,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
let reference_hwnd =
|
||||
monocle.focused_window().copied().unwrap_or_default().hwnd;
|
||||
|
||||
border.update(&rect, true)?;
|
||||
let rect = WindowsApi::window_rect(reference_hwnd)?;
|
||||
|
||||
border.set_position(&rect, reference_hwnd)?;
|
||||
|
||||
border.invalidate();
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
let mut to_remove = vec![];
|
||||
@@ -322,9 +365,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let is_maximized = WindowsApi::is_zoomed(
|
||||
WindowsApi::foreground_window().unwrap_or_default(),
|
||||
);
|
||||
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let foreground_monitor_id =
|
||||
WindowsApi::monitor_from_window(foreground_hwnd);
|
||||
let is_maximized = foreground_monitor_id == m.id()
|
||||
&& WindowsApi::is_zoomed(foreground_hwnd);
|
||||
|
||||
if is_maximized {
|
||||
let mut to_remove = vec![];
|
||||
@@ -345,16 +390,20 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
// Destroy any borders not associated with the focused workspace
|
||||
let container_ids = ws
|
||||
let mut container_and_floating_window_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
.map(|c| c.id().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for w in ws.floating_windows() {
|
||||
container_and_floating_window_ids.push(w.hwnd.to_string());
|
||||
}
|
||||
|
||||
let mut to_remove = vec![];
|
||||
for (id, border) in borders.iter() {
|
||||
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
|
||||
&& !container_ids.contains(id)
|
||||
&& !container_and_floating_window_ids.contains(id)
|
||||
{
|
||||
border.destroy()?;
|
||||
to_remove.push(id.clone());
|
||||
@@ -366,47 +415,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
// Update border when moving or resizing with mouse
|
||||
if pending_move_op.is_some() && idx == ws.focused_container_idx() {
|
||||
let restore_z_order = Z_ORDER.load();
|
||||
Z_ORDER.store(ZOrder::TopMost);
|
||||
|
||||
let mut rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
while WindowsApi::lbutton_is_pressed() {
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(c.id()) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
if rect != new_rect {
|
||||
rect = new_rect;
|
||||
border.update(&rect, true)?;
|
||||
}
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
// Get the border entry for this container from the map or create one
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(c.id()) {
|
||||
if let Ok(border) = Border::create(
|
||||
c.id(),
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
) {
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
@@ -415,6 +431,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
};
|
||||
|
||||
borders_monitors.insert(c.id().clone(), monitor_idx);
|
||||
windows_borders.insert(
|
||||
c.focused_window().cloned().unwrap_or_default().hwnd,
|
||||
border.clone(),
|
||||
);
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
@@ -435,16 +455,79 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
last_focus_state = focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
let reference_hwnd =
|
||||
c.focused_window().copied().unwrap_or_default().hwnd;
|
||||
|
||||
let rect = WindowsApi::window_rect(reference_hwnd)?;
|
||||
|
||||
let should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
border.update(&rect, should_invalidate)?;
|
||||
border.set_position(&rect, reference_hwnd)?;
|
||||
|
||||
if should_invalidate {
|
||||
border.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
for window in ws.floating_windows() {
|
||||
let border = match borders.entry(window.hwnd.to_string()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) =
|
||||
Border::create(&window.hwnd.to_string(), window.hwnd)
|
||||
{
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
|
||||
windows_borders.insert(window.hwnd, border.clone());
|
||||
|
||||
let mut is_focused = false;
|
||||
|
||||
if WindowsApi::foreground_window().unwrap_or_default()
|
||||
== window.hwnd
|
||||
{
|
||||
is_focused = true;
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
let new_focus_state = if is_focused {
|
||||
WindowKind::Floating
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
};
|
||||
|
||||
// Update the focused state for all containers on this workspace
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
last_focus_state =
|
||||
focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
|
||||
let should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
// this has to be sent to match the z-order when clicking on
|
||||
// a floating window to focus it
|
||||
border.set_position(&rect, window.hwnd)?;
|
||||
|
||||
if should_invalidate {
|
||||
border.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,6 +537,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
previous_snapshot = monitors;
|
||||
previous_pending_move_op = pending_move_op;
|
||||
previous_is_paused = is_paused;
|
||||
previous_notification = Some(notification);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -39,6 +39,18 @@ impl From<Color32> for Colour {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Colour> for Color32 {
|
||||
fn from(value: Colour) -> Self {
|
||||
match value {
|
||||
Colour::Rgb(rgb) => Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8),
|
||||
Colour::Hex(hex) => {
|
||||
let rgb = Rgb::from(hex);
|
||||
Color32::from_rgb(rgb.r as u8, rgb.g as u8, rgb.b as u8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct Hex(HexColor);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ impl<'a, T: Clone> ComIn<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Deref for ComIn<'a, T> {
|
||||
impl<T> Deref for ComIn<'_, T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde::Serialize;
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Getters, JsonSchema)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, JsonSchema)]
|
||||
pub struct Container {
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
@@ -27,12 +27,6 @@ impl Default for Container {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Container {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn hide(&self, omit: Option<isize>) {
|
||||
for window in self.windows().iter().rev() {
|
||||
|
||||
135
komorebi/src/core/asc.rs
Normal file
135
komorebi/src/core/asc.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use crate::config_generation::ApplicationConfiguration;
|
||||
use crate::config_generation::ApplicationOptions;
|
||||
use crate::config_generation::MatchingRule;
|
||||
use color_eyre::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ApplicationSpecificConfiguration(pub BTreeMap<String, AscApplicationRulesOrSchema>);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum AscApplicationRulesOrSchema {
|
||||
AscApplicationRules(AscApplicationRules),
|
||||
Schema(String),
|
||||
}
|
||||
|
||||
impl Deref for ApplicationSpecificConfiguration {
|
||||
type Target = BTreeMap<String, AscApplicationRulesOrSchema>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ApplicationSpecificConfiguration {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationSpecificConfiguration {
|
||||
pub fn load(pathbuf: &PathBuf) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(pathbuf)?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
}
|
||||
|
||||
pub fn format(pathbuf: &PathBuf) -> Result<String> {
|
||||
Ok(serde_json::to_string_pretty(&Self::load(pathbuf)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rules that determine how an application is handled
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AscApplicationRules {
|
||||
/// Rules to ignore specific windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore: Option<Vec<MatchingRule>>,
|
||||
/// Rules to forcibly manage specific windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manage: Option<Vec<MatchingRule>>,
|
||||
/// Rules to manage specific windows as floating windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub floating: Option<Vec<MatchingRule>>,
|
||||
/// Rules to ignore specific windows from the transparency feature
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transparency_ignore: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which minimize to the tray or have multiple windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_and_multi_window: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which have the `WS_EX_LAYERED` Extended Window Style
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layered: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which send the `EVENT_OBJECT_NAMECHANGE` event on launch
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_name_change: Option<Vec<MatchingRule>>,
|
||||
/// Rules to identify applications which are slow to send initial event notifications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slow_application: Option<Vec<MatchingRule>>,
|
||||
}
|
||||
|
||||
impl From<Vec<ApplicationConfiguration>> for ApplicationSpecificConfiguration {
|
||||
fn from(value: Vec<ApplicationConfiguration>) -> Self {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
for entry in &value {
|
||||
let key = entry.name.clone();
|
||||
let mut rules = AscApplicationRules {
|
||||
ignore: None,
|
||||
manage: None,
|
||||
floating: None,
|
||||
transparency_ignore: None,
|
||||
tray_and_multi_window: None,
|
||||
layered: None,
|
||||
object_name_change: None,
|
||||
slow_application: None,
|
||||
};
|
||||
|
||||
rules.ignore = entry.ignore_identifiers.clone();
|
||||
|
||||
if let Some(options) = &entry.options {
|
||||
for opt in options {
|
||||
match opt {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
rules.object_name_change =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
rules.layered =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
rules.tray_and_multi_window =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
rules.manage =
|
||||
Some(vec![MatchingRule::Simple(entry.identifier.clone())]);
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rules.ignore.is_some()
|
||||
|| rules.manage.is_some()
|
||||
|| rules.floating.is_some()
|
||||
|| rules.transparency_ignore.is_some()
|
||||
|| rules.tray_and_multi_window.is_some()
|
||||
|| rules.layered.is_some()
|
||||
|| rules.object_name_change.is_some()
|
||||
|| rules.slow_application.is_some()
|
||||
{
|
||||
map.insert(key, AscApplicationRulesOrSchema::AscApplicationRules(rules));
|
||||
}
|
||||
}
|
||||
|
||||
Self(map)
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,8 @@ pub struct ApplicationConfiguration {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub options: Option<Vec<ApplicationOptions>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_identifiers: Option<Vec<MatchingRule>>,
|
||||
#[serde(alias = "float_identifiers")]
|
||||
pub ignore_identifiers: Option<Vec<MatchingRule>>,
|
||||
}
|
||||
|
||||
impl ApplicationConfiguration {
|
||||
@@ -187,7 +188,7 @@ impl ApplicationConfigurationGenerator {
|
||||
|
||||
let mut lines = vec![String::from("# Generated by komorebic.exe"), String::new()];
|
||||
|
||||
let mut float_rules = vec![];
|
||||
let mut ignore_rules = vec![];
|
||||
|
||||
for app in cfgen {
|
||||
lines.push(format!("# {}", app.name));
|
||||
@@ -201,15 +202,15 @@ impl ApplicationConfigurationGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(float_identifiers) = app.float_identifiers {
|
||||
for matching_rule in float_identifiers {
|
||||
if let Some(ignore_identifiers) = app.ignore_identifiers {
|
||||
for matching_rule in ignore_identifiers {
|
||||
if let MatchingRule::Simple(float) = matching_rule {
|
||||
let float_rule =
|
||||
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
|
||||
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
if !ignore_rules.contains(&float_rule) {
|
||||
ignore_rules.push(float_rule.clone());
|
||||
|
||||
// if let Some(comment) = float.comment {
|
||||
// lines.push(format!("# {comment}"));
|
||||
@@ -238,7 +239,7 @@ impl ApplicationConfigurationGenerator {
|
||||
|
||||
let mut lines = vec![String::from("; Generated by komorebic.exe"), String::new()];
|
||||
|
||||
let mut float_rules = vec![];
|
||||
let mut ignore_rules = vec![];
|
||||
|
||||
for app in cfgen {
|
||||
lines.push(format!("; {}", app.name));
|
||||
@@ -252,8 +253,8 @@ impl ApplicationConfigurationGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(float_identifiers) = app.float_identifiers {
|
||||
for matching_rule in float_identifiers {
|
||||
if let Some(ignore_identifiers) = app.ignore_identifiers {
|
||||
for matching_rule in ignore_identifiers {
|
||||
if let MatchingRule::Simple(float) = matching_rule {
|
||||
let float_rule = format!(
|
||||
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
|
||||
@@ -261,8 +262,8 @@ impl ApplicationConfigurationGenerator {
|
||||
);
|
||||
|
||||
// Don't want to send duped signals especially as configs get larger
|
||||
if !float_rules.contains(&float_rule) {
|
||||
float_rules.push(float_rule.clone());
|
||||
if !ignore_rules.contains(&float_rule) {
|
||||
ignore_rules.push(float_rule.clone());
|
||||
|
||||
// if let Some(comment) = float.comment {
|
||||
// lines.push(format!("; {comment}"));
|
||||
|
||||
@@ -14,6 +14,8 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
use crate::KomorebiTheme;
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
@@ -27,6 +29,7 @@ pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
pub mod arrangement;
|
||||
pub mod asc;
|
||||
pub mod config_generation;
|
||||
pub mod custom_layout;
|
||||
pub mod cycle_direction;
|
||||
@@ -47,6 +50,7 @@ pub enum SocketMessage {
|
||||
StackWindow(OperationDirection),
|
||||
UnstackWindow,
|
||||
CycleStack(CycleDirection),
|
||||
CycleStackIndex(CycleDirection),
|
||||
FocusStackWindow(usize),
|
||||
StackAll,
|
||||
UnstackAll,
|
||||
@@ -77,6 +81,7 @@ pub enum SocketMessage {
|
||||
ToggleMonocle,
|
||||
ToggleMaximize,
|
||||
ToggleWindowContainerBehaviour,
|
||||
ToggleFloatOverride,
|
||||
WindowHidingBehaviour(HidingBehaviour),
|
||||
ToggleCrossMonitorMoveBehaviour,
|
||||
CrossMonitorMoveBehaviour(MoveBehaviour),
|
||||
@@ -90,6 +95,8 @@ pub enum SocketMessage {
|
||||
CycleLayout(CycleDirection),
|
||||
ChangeLayoutCustom(PathBuf),
|
||||
FlipLayout(Axis),
|
||||
ToggleWorkspaceWindowContainerBehaviour,
|
||||
ToggleWorkspaceFloatOverride,
|
||||
// Monitor and Workspace Commands
|
||||
MonitorIndexPreference(usize, i32, i32, i32, i32),
|
||||
DisplayIndexPreference(usize, String),
|
||||
@@ -100,6 +107,7 @@ pub enum SocketMessage {
|
||||
Stop,
|
||||
TogglePause,
|
||||
Retile,
|
||||
RetileWithResizeDimensions,
|
||||
QuickSave,
|
||||
QuickLoad,
|
||||
Save(PathBuf),
|
||||
@@ -108,6 +116,7 @@ pub enum SocketMessage {
|
||||
CycleFocusWorkspace(CycleDirection),
|
||||
FocusMonitorNumber(usize),
|
||||
FocusLastWorkspace,
|
||||
CloseWorkspace,
|
||||
FocusWorkspaceNumber(usize),
|
||||
FocusWorkspaceNumbers(usize),
|
||||
FocusMonitorWorkspaceNumber(usize, usize),
|
||||
@@ -138,10 +147,11 @@ pub enum SocketMessage {
|
||||
WatchConfiguration(bool),
|
||||
CompleteConfiguration,
|
||||
AltFocusHack(bool),
|
||||
Animation(bool),
|
||||
AnimationDuration(u64),
|
||||
Theme(KomorebiTheme),
|
||||
Animation(bool, Option<AnimationPrefix>),
|
||||
AnimationDuration(u64, Option<AnimationPrefix>),
|
||||
AnimationFps(u64),
|
||||
AnimationStyle(AnimationStyle),
|
||||
AnimationStyle(AnimationStyle, Option<AnimationPrefix>),
|
||||
#[serde(alias = "ActiveWindowBorder")]
|
||||
Border(bool),
|
||||
#[serde(alias = "ActiveWindowBorderColour")]
|
||||
@@ -174,7 +184,8 @@ pub enum SocketMessage {
|
||||
ClearWorkspaceRules(usize, usize),
|
||||
ClearNamedWorkspaceRules(String),
|
||||
ClearAllWorkspaceRules,
|
||||
FloatRule(ApplicationIdentifier, String),
|
||||
#[serde(alias = "FloatRule")]
|
||||
IgnoreRule(ApplicationIdentifier, String),
|
||||
ManageRule(ApplicationIdentifier, String),
|
||||
IdentifyObjectNameChangeApplication(ApplicationIdentifier, String),
|
||||
IdentifyTrayApplication(ApplicationIdentifier, String),
|
||||
@@ -192,6 +203,7 @@ pub enum SocketMessage {
|
||||
RemoveTitleBar(ApplicationIdentifier, String),
|
||||
ToggleTitleBars,
|
||||
AddSubscriberSocket(String),
|
||||
AddSubscriberSocketWithOptions(String, SubscribeOptions),
|
||||
RemoveSubscriberSocket(String),
|
||||
AddSubscriberPipe(String),
|
||||
RemoveSubscriberPipe(String),
|
||||
@@ -217,6 +229,12 @@ impl FromStr for SocketMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct SubscribeOptions {
|
||||
/// Only emit notifications when the window manager state has changed
|
||||
pub filter_state_changes: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum StackbarMode {
|
||||
Always,
|
||||
@@ -288,12 +306,15 @@ pub enum BorderImplementation {
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
)]
|
||||
pub enum WindowKind {
|
||||
Single,
|
||||
Stack,
|
||||
Monocle,
|
||||
Unfocused,
|
||||
Floating,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -331,7 +352,16 @@ pub enum ApplicationIdentifier {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum FocusFollowsMouseImplementation {
|
||||
/// A custom FFM implementation (slightly more CPU-intensive)
|
||||
@@ -340,18 +370,48 @@ pub enum FocusFollowsMouseImplementation {
|
||||
Windows,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct WindowManagementBehaviour {
|
||||
/// The current WindowContainerBehaviour to be used
|
||||
pub current_behaviour: WindowContainerBehaviour,
|
||||
/// Override of `current_behaviour` to open new windows as floating windows
|
||||
/// that can be later toggled to tiled, when false it will default to
|
||||
/// `current_behaviour` again.
|
||||
pub float_override: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
PartialEq,
|
||||
)]
|
||||
pub enum WindowContainerBehaviour {
|
||||
/// Create a new container for each new window
|
||||
#[default]
|
||||
Create,
|
||||
/// Append new windows to the focused window container
|
||||
Append,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum MoveBehaviour {
|
||||
/// Swap the window container with the window container at the edge of the adjacent monitor
|
||||
@@ -385,7 +445,16 @@ pub enum HidingBehaviour {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
ValueEnum,
|
||||
JsonSchema,
|
||||
)]
|
||||
pub enum OperationBehaviour {
|
||||
/// Process komorebic commands on temporarily unmanaged/floated windows
|
||||
|
||||
@@ -37,6 +37,12 @@ impl From<Rect> for RECT {
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||
self.right == rhs.right && self.bottom == rhs.bottom
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// decrease the size of self by the padding amount.
|
||||
pub fn add_padding<T>(&mut self, padding: T)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
pub mod animation;
|
||||
pub mod animation_manager;
|
||||
pub mod border_manager;
|
||||
pub mod com;
|
||||
#[macro_use]
|
||||
@@ -20,6 +19,7 @@ pub mod set_window_position;
|
||||
pub mod stackbar_manager;
|
||||
pub mod static_config;
|
||||
pub mod styles;
|
||||
pub mod theme_manager;
|
||||
pub mod transparency_manager;
|
||||
pub mod window;
|
||||
pub mod window_manager;
|
||||
@@ -46,8 +46,6 @@ use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use animation::*;
|
||||
pub use animation_manager::*;
|
||||
pub use colour::*;
|
||||
pub use core::*;
|
||||
pub use process_command::*;
|
||||
@@ -139,7 +137,7 @@ lazy_static! {
|
||||
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref MANAGE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![]));
|
||||
static ref FLOAT_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
static ref IGNORE_IDENTIFIERS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
// mstsc.exe creates these on Windows 11 when a WSL process is launched
|
||||
// https://github.com/LGUG2Z/komorebi/issues/74
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
@@ -158,6 +156,7 @@ lazy_static! {
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
]));
|
||||
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
@@ -176,6 +175,8 @@ lazy_static! {
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
pub static ref SUBSCRIPTION_SOCKET_OPTIONS: Arc<Mutex<HashMap<String, SubscribeOptions>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref TCP_CONNECTIONS: Arc<Mutex<HashMap<String, TcpStream>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
static ref HIDING_BEHAVIOUR: Arc<Mutex<HidingBehaviour>> =
|
||||
@@ -212,19 +213,12 @@ lazy_static! {
|
||||
)
|
||||
};
|
||||
|
||||
static ref ANIMATION_STYLE: Arc<Mutex<AnimationStyle >> =
|
||||
Arc::new(Mutex::new(AnimationStyle::Linear));
|
||||
|
||||
static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
|
||||
Arc::new(Mutex::new(AnimationManager::new()));
|
||||
|
||||
// Use app-specific titlebar removal options where possible
|
||||
// eg. Windows Terminal, IntelliJ IDEA, Firefox
|
||||
static ref NO_TITLEBAR: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
static ref WINDOWS_BY_BAR_HWNDS: Arc<Mutex<HashMap<isize, VecDeque<isize>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
@@ -235,8 +229,6 @@ pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
|
||||
pub static ANIMATION_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
pub static ANIMATION_DURATION: AtomicU64 = AtomicU64::new(250);
|
||||
|
||||
pub static SLOW_APPLICATION_COMPENSATION_TIME: AtomicU64 = AtomicU64::new(20);
|
||||
|
||||
@@ -297,18 +289,38 @@ pub struct Notification {
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
|
||||
let is_override_event = matches!(
|
||||
notification.event,
|
||||
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
|
||||
| NotificationEvent::Socket(SocketMessage::AddSubscriberSocketWithOptions(_, _))
|
||||
| NotificationEvent::Socket(SocketMessage::Theme(_))
|
||||
| NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(_))
|
||||
| NotificationEvent::WindowManager(WindowManagerEvent::TitleUpdate(_, _))
|
||||
| NotificationEvent::WindowManager(WindowManagerEvent::Show(_, _))
|
||||
);
|
||||
|
||||
let notification = &serde_json::to_string(¬ification)?;
|
||||
let mut stale_sockets = vec![];
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
let options = SUBSCRIPTION_SOCKET_OPTIONS.lock();
|
||||
|
||||
for (socket, path) in &mut *sockets {
|
||||
match UnixStream::connect(path) {
|
||||
Ok(mut stream) => {
|
||||
tracing::debug!("pushed notification to subscriber: {socket}");
|
||||
stream.write_all(notification.as_bytes())?;
|
||||
}
|
||||
Err(_) => {
|
||||
stale_sockets.push(socket.clone());
|
||||
let apply_state_filter = (*options)
|
||||
.get(socket)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.filter_state_changes;
|
||||
|
||||
if !apply_state_filter || state_has_been_modified || is_override_event {
|
||||
match UnixStream::connect(path) {
|
||||
Ok(mut stream) => {
|
||||
tracing::debug!("pushed notification to subscriber: {socket}");
|
||||
stream.write_all(notification.as_bytes())?;
|
||||
}
|
||||
Err(_) => {
|
||||
stale_sockets.push(socket.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,6 +328,13 @@ pub fn notify_subscribers(notification: &str) -> Result<()> {
|
||||
for socket in stale_sockets {
|
||||
tracing::warn!("removing stale subscription: {socket}");
|
||||
sockets.remove(&socket);
|
||||
let socket_path = DATA_DIR.join(socket);
|
||||
if let Err(error) = std::fs::remove_file(&socket_path) {
|
||||
tracing::error!(
|
||||
"could not remove stale subscriber socket file at {}: {error}",
|
||||
socket_path.display()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let mut stale_pipes = vec![];
|
||||
|
||||
@@ -17,6 +17,9 @@ use std::time::Duration;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_utils::Backoff;
|
||||
use komorebi::animation::AnimationEngine;
|
||||
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
#[cfg(feature = "deadlock_detection")]
|
||||
use parking_lot::deadlock;
|
||||
use parking_lot::Mutex;
|
||||
@@ -38,6 +41,7 @@ use komorebi::process_movement::listen_for_movements;
|
||||
use komorebi::reaper;
|
||||
use komorebi::stackbar_manager;
|
||||
use komorebi::static_config::StaticConfig;
|
||||
use komorebi::theme_manager;
|
||||
use komorebi::transparency_manager;
|
||||
use komorebi::window_manager::WindowManager;
|
||||
use komorebi::windows_api::WindowsApi;
|
||||
@@ -271,6 +275,7 @@ fn main() -> Result<()> {
|
||||
monitor_reconciliator::listen_for_notifications(wm.clone())?;
|
||||
reaper::watch_for_orphans(wm.clone());
|
||||
focus_manager::listen_for_notifications(wm.clone());
|
||||
theme_manager::listen_for_notifications();
|
||||
|
||||
let (ctrlc_sender, ctrlc_receiver) = crossbeam_channel::bounded(1);
|
||||
ctrlc::set_handler(move || {
|
||||
@@ -285,7 +290,10 @@ fn main() -> Result<()> {
|
||||
|
||||
tracing::error!("received ctrl-c, restoring all hidden windows and terminating process");
|
||||
|
||||
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
|
||||
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
|
||||
wm.lock().restore_all_windows()?;
|
||||
AnimationEngine::wait_for_all_animations();
|
||||
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::workspace::Workspace;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Layout;
|
||||
use crate::OperationDirection;
|
||||
use crate::WindowsApi;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -138,6 +139,86 @@ impl Monitor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a container to this `Monitor` using the move direction to calculate if the container
|
||||
/// should be added in front of all containers, in the back or in place of the focused
|
||||
/// container, moving the rest along. The move direction should be from the origin monitor
|
||||
/// towards the target monitor or from the origin workspace towards the target workspace.
|
||||
pub fn add_container_with_direction(
|
||||
&mut self,
|
||||
container: Container,
|
||||
workspace_idx: Option<usize>,
|
||||
direction: OperationDirection,
|
||||
) -> Result<()> {
|
||||
let workspace = if let Some(idx) = workspace_idx {
|
||||
self.workspaces_mut()
|
||||
.get_mut(idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
|
||||
} else {
|
||||
self.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?
|
||||
};
|
||||
|
||||
match direction {
|
||||
OperationDirection::Left => {
|
||||
// insert the container into the workspace on the monitor at the back (or rightmost position)
|
||||
// if we are moving across a boundary to the left (back = right side of the target)
|
||||
match workspace.layout() {
|
||||
Layout::Default(layout) => match layout {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
workspace.add_container_to_front(container);
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack => {
|
||||
if workspace.containers().len() == 1 {
|
||||
workspace.insert_container_at_idx(0, container);
|
||||
} else {
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
},
|
||||
Layout::Custom(_) => {
|
||||
workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
// insert the container into the workspace on the monitor at the front (or leftmost position)
|
||||
// if we are moving across a boundary to the right (front = left side of the target)
|
||||
match workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index = layout.leftmost_index(workspace.containers().len());
|
||||
|
||||
match layout {
|
||||
DefaultLayout::RightMainVerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => {
|
||||
if workspace.containers().len() == 1 {
|
||||
workspace.add_container_to_back(container);
|
||||
} else {
|
||||
workspace.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
workspace.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
workspace.add_container_to_front(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
OperationDirection::Up | OperationDirection::Down => {
|
||||
// insert the container into the workspace on the monitor at the position
|
||||
// where the currently focused container on that workspace is
|
||||
workspace.insert_container_at_idx(workspace.focused_container_idx(), container);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_workspace_by_idx(&mut self, idx: usize) -> Option<Workspace> {
|
||||
if idx < self.workspaces().len() {
|
||||
return self.workspaces_mut().remove(idx);
|
||||
@@ -178,65 +259,49 @@ impl Monitor {
|
||||
bail!("cannot move native maximized window to another monitor or workspace");
|
||||
}
|
||||
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
let foreground_hwnd = WindowsApi::foreground_window()?;
|
||||
let floating_window_index = workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.position(|w| w.hwnd == foreground_hwnd);
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
if let Some(idx) = floating_window_index {
|
||||
let window = workspace.floating_windows_mut().remove(idx);
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
match direction {
|
||||
Some(OperationDirection::Left) => match target_workspace.layout() {
|
||||
Layout::Default(layout) => match layout {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
target_workspace.add_container_to_front(container);
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace.insert_container_at_idx(0, container);
|
||||
} else {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
},
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
let workspaces = self.workspaces_mut();
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
},
|
||||
Some(OperationDirection::Right) => match target_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index = layout.leftmost_index(target_workspace.containers().len());
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
match layout {
|
||||
DefaultLayout::RightMainVerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace.add_container_to_back(container);
|
||||
} else {
|
||||
target_workspace.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
target_workspace.floating_windows_mut().push(window);
|
||||
} else {
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_front(container);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
if let Some(direction) = direction {
|
||||
self.add_container_with_direction(
|
||||
container,
|
||||
Some(target_workspace_idx),
|
||||
direction,
|
||||
)?;
|
||||
} else {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
);
|
||||
|
||||
ACTIVE.store(true, Ordering::SeqCst);
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
|
||||
continue 'receiver;
|
||||
@@ -172,7 +173,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
if should_update {
|
||||
tracing::info!("updated work area for {}", monitor.device_id());
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"work areas match, reconciliation not required for {}",
|
||||
@@ -219,7 +220,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
);
|
||||
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"resolutions match, reconciliation not required for {}",
|
||||
@@ -406,7 +407,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Second retile to fix DPI/resolution related jank
|
||||
wm.retile_all(true)?;
|
||||
// Border updates to fix DPI/resolution related jank
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ use schemars::gen::SchemaSettings;
|
||||
use schemars::schema_for;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
use crate::animation::AnimationEngine;
|
||||
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
|
||||
use crate::core::config_generation::ApplicationConfiguration;
|
||||
use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
@@ -40,6 +44,10 @@ use crate::core::StateQuery;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowKind;
|
||||
|
||||
use crate::animation::ANIMATION_DURATION_GLOBAL;
|
||||
use crate::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use crate::animation::ANIMATION_FPS;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::IMPLEMENTATION;
|
||||
use crate::border_manager::STYLE;
|
||||
@@ -51,6 +59,7 @@ use crate::stackbar_manager;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::theme_manager;
|
||||
use crate::transparency_manager;
|
||||
use crate::window::RuleDebug;
|
||||
use crate::window::Window;
|
||||
@@ -61,15 +70,12 @@ use crate::winevent_listener;
|
||||
use crate::GlobalState;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_ENABLED;
|
||||
use crate::ANIMATION_FPS;
|
||||
use crate::ANIMATION_STYLE;
|
||||
use crate::State;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
@@ -79,6 +85,7 @@ use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::SUBSCRIPTION_PIPES;
|
||||
use crate::SUBSCRIPTION_SOCKETS;
|
||||
use crate::SUBSCRIPTION_SOCKET_OPTIONS;
|
||||
use crate::TCP_CONNECTIONS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WINDOWS_11;
|
||||
@@ -106,10 +113,19 @@ pub fn listen_for_commands(wm: Arc<Mutex<WindowManager>>) {
|
||||
tracing::info!("listening on komorebi.sock");
|
||||
for client in listener.incoming() {
|
||||
match client {
|
||||
Ok(stream) => match read_commands_uds(&wm, stream) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
},
|
||||
Ok(stream) => {
|
||||
let wm_clone = wm.clone();
|
||||
std::thread::spawn(move || {
|
||||
match stream.set_read_timeout(Some(Duration::from_secs(1))) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
match read_commands_uds(&wm_clone, stream) {
|
||||
Ok(()) => {}
|
||||
Err(error) => tracing::error!("{}", error),
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{}", error);
|
||||
break;
|
||||
@@ -187,6 +203,10 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_asref)]
|
||||
// We don't have From implemented for &mut WindowManager
|
||||
let initial_state = State::from(self.as_ref());
|
||||
|
||||
match message {
|
||||
SocketMessage::CycleFocusWorkspace(_) | SocketMessage::FocusWorkspaceNumber(_) => {
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
@@ -231,7 +251,18 @@ impl WindowManager {
|
||||
self.cycle_container_window_in_direction(direction)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
SocketMessage::CycleStackIndex(direction) => {
|
||||
self.cycle_container_window_index_in_direction(direction)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
SocketMessage::FocusStackWindow(idx) => {
|
||||
// In case you are using this command on a bar on a monitor
|
||||
// different from the currently focused one, you'd want that
|
||||
// monitor to be focused so that the FocusStackWindow happens
|
||||
// on the monitor with the bar you just pressed.
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
self.focus_container_window(idx)?;
|
||||
self.focused_window()?.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
@@ -387,20 +418,20 @@ impl WindowManager {
|
||||
}));
|
||||
}
|
||||
}
|
||||
SocketMessage::FloatRule(identifier, ref id) => {
|
||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
SocketMessage::IgnoreRule(identifier, ref id) => {
|
||||
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
|
||||
|
||||
let mut should_push = true;
|
||||
for f in &*float_identifiers {
|
||||
if let MatchingRule::Simple(f) = f {
|
||||
if f.id.eq(id) {
|
||||
for i in &*ignore_identifiers {
|
||||
if let MatchingRule::Simple(i) = i {
|
||||
if i.id.eq(id) {
|
||||
should_push = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_push {
|
||||
float_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
ignore_identifiers.push(MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: identifier,
|
||||
id: id.clone(),
|
||||
matching_strategy: Option::from(MatchingStrategy::Legacy),
|
||||
@@ -505,7 +536,8 @@ impl WindowManager {
|
||||
self.move_container_to_workspace(workspace_idx, true, None)?;
|
||||
}
|
||||
SocketMessage::MoveContainerToMonitorNumber(monitor_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, None, true)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
|
||||
}
|
||||
SocketMessage::SwapWorkspacesToMonitorNumber(monitor_idx) => {
|
||||
self.swap_focused_monitor(monitor_idx)?;
|
||||
@@ -517,7 +549,8 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
|
||||
);
|
||||
|
||||
self.move_container_to_monitor(monitor_idx, None, true)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(monitor_idx, None, true, direction)?;
|
||||
}
|
||||
SocketMessage::SendContainerToWorkspaceNumber(workspace_idx) => {
|
||||
self.move_container_to_workspace(workspace_idx, false, None)?;
|
||||
@@ -539,7 +572,8 @@ impl WindowManager {
|
||||
self.move_container_to_workspace(workspace_idx, false, None)?;
|
||||
}
|
||||
SocketMessage::SendContainerToMonitorNumber(monitor_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, None, false)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
|
||||
}
|
||||
SocketMessage::CycleSendContainerToMonitor(direction) => {
|
||||
let monitor_idx = direction.next_idx(
|
||||
@@ -548,22 +582,37 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there must be at least one monitor"))?,
|
||||
);
|
||||
|
||||
self.move_container_to_monitor(monitor_idx, None, false)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(monitor_idx, None, false, direction)?;
|
||||
}
|
||||
SocketMessage::SendContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), false)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(
|
||||
monitor_idx,
|
||||
Option::from(workspace_idx),
|
||||
false,
|
||||
direction,
|
||||
)?;
|
||||
}
|
||||
SocketMessage::MoveContainerToMonitorWorkspaceNumber(monitor_idx, workspace_idx) => {
|
||||
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(
|
||||
monitor_idx,
|
||||
Option::from(workspace_idx),
|
||||
true,
|
||||
direction,
|
||||
)?;
|
||||
}
|
||||
SocketMessage::SendContainerToNamedWorkspace(ref workspace) => {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
self.monitor_workspace_index_by_name(workspace)
|
||||
{
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(
|
||||
monitor_idx,
|
||||
Option::from(workspace_idx),
|
||||
false,
|
||||
direction,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -571,7 +620,13 @@ impl WindowManager {
|
||||
if let Some((monitor_idx, workspace_idx)) =
|
||||
self.monitor_workspace_index_by_name(workspace)
|
||||
{
|
||||
self.move_container_to_monitor(monitor_idx, Option::from(workspace_idx), true)?;
|
||||
let direction = self.direction_from_monitor_idx(monitor_idx);
|
||||
self.move_container_to_monitor(
|
||||
monitor_idx,
|
||||
Option::from(workspace_idx),
|
||||
true,
|
||||
direction,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,10 +670,13 @@ impl WindowManager {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
||||
}
|
||||
SocketMessage::Retile => {
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
border_manager::destroy_all_borders()?;
|
||||
self.retile_all(false)?
|
||||
}
|
||||
SocketMessage::RetileWithResizeDimensions => {
|
||||
border_manager::destroy_all_borders()?;
|
||||
self.retile_all(true)?
|
||||
}
|
||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
||||
@@ -742,6 +800,44 @@ impl WindowManager {
|
||||
|
||||
self.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
SocketMessage::CloseWorkspace => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
// the workspace switch op
|
||||
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
|
||||
self.focus_monitor(monitor_idx)?;
|
||||
}
|
||||
|
||||
let mut can_close = false;
|
||||
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
let last_focused_workspace = monitor
|
||||
.last_focused_workspace()
|
||||
.unwrap_or(focused_workspace_idx.saturating_sub(1));
|
||||
|
||||
if let Some(workspace) = monitor.focused_workspace() {
|
||||
if monitor.workspaces().len() > 1
|
||||
&& workspace.containers().is_empty()
|
||||
&& workspace.floating_windows().is_empty()
|
||||
&& workspace.monocle_container().is_none()
|
||||
&& workspace.maximized_window().is_none()
|
||||
&& workspace.name().is_none()
|
||||
{
|
||||
can_close = true;
|
||||
}
|
||||
}
|
||||
|
||||
if can_close
|
||||
&& monitor
|
||||
.workspaces_mut()
|
||||
.remove(focused_workspace_idx)
|
||||
.is_some()
|
||||
{
|
||||
self.focus_workspace(last_focused_workspace)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusLastWorkspace => {
|
||||
// This is to ensure that even on an empty workspace on a secondary monitor, the
|
||||
// secondary monitor where the cursor is focused will be used as the target for
|
||||
@@ -819,7 +915,11 @@ impl WindowManager {
|
||||
tracing::info!(
|
||||
"received stop command, restoring all hidden windows and terminating process"
|
||||
);
|
||||
|
||||
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
|
||||
ANIMATION_ENABLED_GLOBAL.store(false, Ordering::SeqCst);
|
||||
self.restore_all_windows()?;
|
||||
AnimationEngine::wait_for_all_animations();
|
||||
|
||||
if WindowsApi::focus_follows_mouse()? {
|
||||
WindowsApi::disable_focus_follows_mouse()?;
|
||||
@@ -1312,6 +1412,14 @@ impl WindowManager {
|
||||
let socket_path = DATA_DIR.join(socket);
|
||||
sockets.insert(socket.clone(), socket_path);
|
||||
}
|
||||
SocketMessage::AddSubscriberSocketWithOptions(ref socket, options) => {
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
let socket_path = DATA_DIR.join(socket);
|
||||
sockets.insert(socket.clone(), socket_path);
|
||||
|
||||
let mut socket_options = SUBSCRIPTION_SOCKET_OPTIONS.lock();
|
||||
socket_options.insert(socket.clone(), options);
|
||||
}
|
||||
SocketMessage::RemoveSubscriberSocket(ref socket) => {
|
||||
let mut sockets = SUBSCRIPTION_SOCKETS.lock();
|
||||
sockets.remove(socket);
|
||||
@@ -1339,15 +1447,52 @@ impl WindowManager {
|
||||
self.resize_delta = delta;
|
||||
}
|
||||
SocketMessage::ToggleWindowContainerBehaviour => {
|
||||
match self.window_container_behaviour {
|
||||
match self.window_management_behaviour.current_behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
self.window_container_behaviour = WindowContainerBehaviour::Append;
|
||||
self.window_management_behaviour.current_behaviour =
|
||||
WindowContainerBehaviour::Append;
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
self.window_container_behaviour = WindowContainerBehaviour::Create;
|
||||
self.window_management_behaviour.current_behaviour =
|
||||
WindowContainerBehaviour::Create;
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleFloatOverride => {
|
||||
self.window_management_behaviour.float_override =
|
||||
!self.window_management_behaviour.float_override;
|
||||
}
|
||||
SocketMessage::ToggleWorkspaceWindowContainerBehaviour => {
|
||||
let current_global_behaviour = self.window_management_behaviour.current_behaviour;
|
||||
if let Some(behaviour) = self
|
||||
.focused_workspace_mut()?
|
||||
.window_container_behaviour_mut()
|
||||
{
|
||||
match behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
*behaviour = WindowContainerBehaviour::Append
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
*behaviour = WindowContainerBehaviour::Create
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.focused_workspace_mut()?
|
||||
.set_window_container_behaviour(Some(match current_global_behaviour {
|
||||
WindowContainerBehaviour::Create => WindowContainerBehaviour::Append,
|
||||
WindowContainerBehaviour::Append => WindowContainerBehaviour::Create,
|
||||
}));
|
||||
};
|
||||
}
|
||||
SocketMessage::ToggleWorkspaceFloatOverride => {
|
||||
let current_global_override = self.window_management_behaviour.float_override;
|
||||
if let Some(float_override) = self.focused_workspace_mut()?.float_override_mut() {
|
||||
*float_override = !*float_override;
|
||||
} else {
|
||||
self.focused_workspace_mut()?
|
||||
.set_float_override(Some(!current_global_override));
|
||||
};
|
||||
}
|
||||
SocketMessage::WindowHidingBehaviour(behaviour) => {
|
||||
let mut hiding_behaviour = HIDING_BEHAVIOUR.lock();
|
||||
*hiding_behaviour = behaviour;
|
||||
@@ -1371,6 +1516,16 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::Border(enable) => {
|
||||
border_manager::BORDER_ENABLED.store(enable, Ordering::SeqCst);
|
||||
if !enable {
|
||||
match IMPLEMENTATION.load() {
|
||||
BorderImplementation::Komorebi => {
|
||||
border_manager::destroy_all_borders()?;
|
||||
}
|
||||
BorderImplementation::Windows => {
|
||||
self.remove_all_accents()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::BorderImplementation(implementation) => {
|
||||
if !*WINDOWS_11 && matches!(implementation, BorderImplementation::Windows) {
|
||||
@@ -1388,7 +1543,7 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
}
|
||||
SocketMessage::BorderColour(kind, r, g, b) => match kind {
|
||||
@@ -1404,6 +1559,9 @@ impl WindowManager {
|
||||
WindowKind::Unfocused => {
|
||||
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
WindowKind::Floating => {
|
||||
border_manager::FLOATING.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
|
||||
}
|
||||
},
|
||||
SocketMessage::BorderStyle(style) => {
|
||||
STYLE.store(style);
|
||||
@@ -1414,18 +1572,41 @@ impl WindowManager {
|
||||
SocketMessage::BorderOffset(offset) => {
|
||||
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::Animation(enable) => {
|
||||
ANIMATION_ENABLED.store(enable, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::AnimationDuration(duration) => {
|
||||
ANIMATION_DURATION.store(duration, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::Animation(enable, prefix) => match prefix {
|
||||
Some(prefix) => {
|
||||
ANIMATION_ENABLED_PER_ANIMATION
|
||||
.lock()
|
||||
.insert(prefix, enable);
|
||||
}
|
||||
None => {
|
||||
ANIMATION_ENABLED_GLOBAL.store(enable, Ordering::SeqCst);
|
||||
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
},
|
||||
SocketMessage::AnimationDuration(duration, prefix) => match prefix {
|
||||
Some(prefix) => {
|
||||
ANIMATION_DURATION_PER_ANIMATION
|
||||
.lock()
|
||||
.insert(prefix, duration);
|
||||
}
|
||||
None => {
|
||||
ANIMATION_DURATION_GLOBAL.store(duration, Ordering::SeqCst);
|
||||
ANIMATION_DURATION_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
},
|
||||
SocketMessage::AnimationFps(fps) => {
|
||||
ANIMATION_FPS.store(fps, Ordering::SeqCst);
|
||||
}
|
||||
SocketMessage::AnimationStyle(style) => {
|
||||
*ANIMATION_STYLE.lock() = style;
|
||||
}
|
||||
SocketMessage::AnimationStyle(style, prefix) => match prefix {
|
||||
Some(prefix) => {
|
||||
ANIMATION_STYLE_PER_ANIMATION.lock().insert(prefix, style);
|
||||
}
|
||||
None => {
|
||||
let mut animation_style = ANIMATION_STYLE_GLOBAL.lock();
|
||||
*animation_style = style;
|
||||
ANIMATION_STYLE_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
},
|
||||
SocketMessage::ToggleTransparency => {
|
||||
let current = transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst);
|
||||
transparency_manager::TRANSPARENCY_ENABLED.store(!current, Ordering::SeqCst);
|
||||
@@ -1522,18 +1703,23 @@ impl WindowManager {
|
||||
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
theme_manager::send_notification(theme);
|
||||
}
|
||||
// Deprecated commands
|
||||
SocketMessage::AltFocusHack(_)
|
||||
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}
|
||||
};
|
||||
|
||||
let notification = Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: self.as_ref().into(),
|
||||
};
|
||||
notify_subscribers(
|
||||
Notification {
|
||||
event: NotificationEvent::Socket(message.clone()),
|
||||
state: self.as_ref().into(),
|
||||
},
|
||||
initial_state.has_been_modified(self.as_ref()),
|
||||
)?;
|
||||
|
||||
notify_subscribers(&serde_json::to_string(¬ification)?)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
transparency_manager::send_notification();
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
@@ -1551,22 +1737,29 @@ pub fn read_commands_uds(wm: &Arc<Mutex<WindowManager>>, mut stream: UnixStream)
|
||||
for line in reader.lines() {
|
||||
let message = SocketMessage::from_str(&line?)?;
|
||||
|
||||
let mut wm = wm.lock();
|
||||
|
||||
if wm.is_paused {
|
||||
return match message {
|
||||
SocketMessage::TogglePause
|
||||
| SocketMessage::State
|
||||
| SocketMessage::GlobalState
|
||||
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
|
||||
_ => {
|
||||
tracing::trace!("ignoring while paused");
|
||||
Ok(())
|
||||
match wm.try_lock_for(Duration::from_secs(1)) {
|
||||
None => {
|
||||
tracing::warn!(
|
||||
"could not acquire window manager lock, not processing message: {message}"
|
||||
);
|
||||
}
|
||||
Some(mut wm) => {
|
||||
if wm.is_paused {
|
||||
return match message {
|
||||
SocketMessage::TogglePause
|
||||
| SocketMessage::State
|
||||
| SocketMessage::GlobalState
|
||||
| SocketMessage::Stop => Ok(wm.process_command(message, &mut stream)?),
|
||||
_ => {
|
||||
tracing::trace!("ignoring while paused");
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
wm.process_command(message.clone(), &mut stream)?;
|
||||
wm.process_command(message.clone(), &mut stream)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -32,7 +32,9 @@ use crate::workspace_reconciliator::ALT_TAB_HWND;
|
||||
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::DATA_DIR;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
@@ -101,6 +103,10 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
if !transparency_override {
|
||||
if rule_debug.matches_ignore_identifier.is_some() {
|
||||
border_manager::send_notification(Option::from(event.hwnd()));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -117,6 +123,10 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_asref)]
|
||||
// We don't have From implemented for &mut WindowManager
|
||||
let initial_state = State::from(self.as_ref());
|
||||
|
||||
// Make sure we have the most recently focused monitor from any event
|
||||
match event {
|
||||
WindowManagerEvent::FocusChange(_, window)
|
||||
@@ -149,14 +159,6 @@ impl WindowManager {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
for monitor in self.monitors_mut() {
|
||||
for workspace in monitor.workspaces_mut() {
|
||||
if let WindowManagerEvent::FocusChange(_, window) = event {
|
||||
let _ = workspace.focus_changed(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.enforce_workspace_rules()?;
|
||||
|
||||
if matches!(event, WindowManagerEvent::MouseCapture(..)) {
|
||||
@@ -226,8 +228,7 @@ impl WindowManager {
|
||||
.is_some();
|
||||
|
||||
if !window.is_window()
|
||||
|| should_act
|
||||
|| !programmatically_hidden_hwnds.contains(&window.hwnd)
|
||||
|| (should_act && !programmatically_hidden_hwnds.contains(&window.hwnd))
|
||||
{
|
||||
hide = true;
|
||||
}
|
||||
@@ -246,155 +247,221 @@ impl WindowManager {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if !workspace
|
||||
let floating_window_idx = workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.any(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
if let Some(w) = workspace.maximized_window() {
|
||||
if w.hwnd == window.hwnd {
|
||||
return Ok(());
|
||||
.position(|w| w.hwnd == window.hwnd);
|
||||
|
||||
match floating_window_idx {
|
||||
None => {
|
||||
if let Some(w) = workspace.maximized_window() {
|
||||
if w.hwnd == window.hwnd {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
window.focus(false)?;
|
||||
}
|
||||
} else {
|
||||
workspace.focus_container_by_window(window.hwnd)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(monocle) = workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
Some(idx) => {
|
||||
if let Some(window) = workspace.floating_windows().get(idx) {
|
||||
window.focus(false)?;
|
||||
}
|
||||
} else {
|
||||
self.focused_workspace_mut()?
|
||||
.focus_container_by_window(window.hwnd)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::Show(_, window)
|
||||
| WindowManagerEvent::Manage(window)
|
||||
| WindowManagerEvent::Uncloak(_, window) => {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
if matches!(event, WindowManagerEvent::Uncloak(_, _))
|
||||
&& self.uncloack_to_ignore >= 1
|
||||
{
|
||||
tracing::info!("ignoring uncloak after monocle move by mouse across monitors");
|
||||
self.uncloack_to_ignore = self.uncloack_to_ignore.saturating_sub(1);
|
||||
} else {
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx =
|
||||
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
|
||||
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
let mut needs_reconciliation = false;
|
||||
let mut needs_reconciliation = false;
|
||||
|
||||
for (i, monitors) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitors.workspaces().iter().enumerate() {
|
||||
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
|
||||
// At this point we know we are going to send a notification to the workspace reconciliator
|
||||
// So we get the topmost window returned by EnumWindows, which is almost always the window
|
||||
// that has been selected by alt-tab
|
||||
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
|
||||
if let Some(first) =
|
||||
alt_tab_windows.iter().find(|w| w.title().is_ok())
|
||||
for (i, monitors) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitors.workspaces().iter().enumerate() {
|
||||
if workspace.contains_window(window.hwnd) && focused_pair != (i, j) {
|
||||
// At this point we know we are going to send a notification to the workspace reconciliator
|
||||
// So we get the topmost window returned by EnumWindows, which is almost always the window
|
||||
// that has been selected by alt-tab
|
||||
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
|
||||
if let Some(first) =
|
||||
alt_tab_windows.iter().find(|w| w.title().is_ok())
|
||||
{
|
||||
// If our record of this HWND hasn't been updated in over a minute
|
||||
let mut instant = ALT_TAB_HWND_INSTANT.lock();
|
||||
if instant.elapsed().gt(&Duration::from_secs(1)) {
|
||||
// Update our record with the HWND we just found
|
||||
ALT_TAB_HWND.store(Some(first.hwnd));
|
||||
// Update the timestamp of our record
|
||||
*instant = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace_reconciliator::send_notification(i, j);
|
||||
needs_reconciliation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There are some applications such as Firefox where, if they are focused when a
|
||||
// workspace switch takes place, it will fire an additional Show event, which will
|
||||
// result in them being associated with both the original workspace and the workspace
|
||||
// being switched to. This loop is to try to ensure that we don't end up with
|
||||
// duplicates across multiple workspaces, as it results in ghost layout tiles.
|
||||
let mut proceed = true;
|
||||
|
||||
for (i, monitor) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
if workspace.contains_window(window.hwnd)
|
||||
&& i != self.focused_monitor_idx()
|
||||
&& j != monitor.focused_workspace_idx()
|
||||
{
|
||||
tracing::debug!(
|
||||
"ignoring show event for window already associated with another workspace"
|
||||
);
|
||||
|
||||
window.hide();
|
||||
proceed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if proceed {
|
||||
let mut behaviour = self.window_management_behaviour(
|
||||
focused_monitor_idx,
|
||||
focused_workspace_idx,
|
||||
);
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let workspace_contains_window = workspace.contains_window(window.hwnd);
|
||||
let monocle_container = workspace.monocle_container().clone();
|
||||
|
||||
if !workspace_contains_window && !needs_reconciliation {
|
||||
let floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
let mut should_float = false;
|
||||
|
||||
if !floating_applications.is_empty() {
|
||||
if let (Ok(title), Ok(exe_name), Ok(class), Ok(path)) =
|
||||
(window.title(), window.exe(), window.class(), window.path())
|
||||
{
|
||||
// If our record of this HWND hasn't been updated in over a minute
|
||||
let mut instant = ALT_TAB_HWND_INSTANT.lock();
|
||||
if instant.elapsed().gt(&Duration::from_secs(1)) {
|
||||
// Update our record with the HWND we just found
|
||||
ALT_TAB_HWND.store(Some(first.hwnd));
|
||||
// Update the timestamp of our record
|
||||
*instant = Instant::now();
|
||||
should_float = should_act(
|
||||
&title,
|
||||
&exe_name,
|
||||
&class,
|
||||
&path,
|
||||
&floating_applications,
|
||||
®ex_identifiers,
|
||||
)
|
||||
.is_some();
|
||||
}
|
||||
}
|
||||
|
||||
behaviour.float_override = behaviour.float_override
|
||||
|| (should_float
|
||||
&& !matches!(event, WindowManagerEvent::Manage(_)));
|
||||
|
||||
if behaviour.float_override {
|
||||
workspace.floating_windows_mut().push(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
} else {
|
||||
match behaviour.current_behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
workspace.new_container_for_window(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
workspace
|
||||
.focused_container_mut()
|
||||
.ok_or_else(|| {
|
||||
anyhow!("there is no focused container")
|
||||
})?
|
||||
.add_window(window);
|
||||
self.update_focused_workspace(true, false)?;
|
||||
stackbar_manager::send_notification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workspace_reconciliator::send_notification(i, j);
|
||||
needs_reconciliation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There are some applications such as Firefox where, if they are focused when a
|
||||
// workspace switch takes place, it will fire an additional Show event, which will
|
||||
// result in them being associated with both the original workspace and the workspace
|
||||
// being switched to. This loop is to try to ensure that we don't end up with
|
||||
// duplicates across multiple workspaces, as it results in ghost layout tiles.
|
||||
let mut proceed = true;
|
||||
|
||||
for (i, monitor) in self.monitors().iter().enumerate() {
|
||||
for (j, workspace) in monitor.workspaces().iter().enumerate() {
|
||||
if workspace.container_for_window(window.hwnd).is_some()
|
||||
&& i != self.focused_monitor_idx()
|
||||
&& j != monitor.focused_workspace_idx()
|
||||
{
|
||||
tracing::debug!(
|
||||
"ignoring show event for window already associated with another workspace"
|
||||
);
|
||||
|
||||
window.hide();
|
||||
proceed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if proceed {
|
||||
let behaviour =
|
||||
self.window_container_behaviour(focused_monitor_idx, focused_workspace_idx);
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let workspace_contains_window = workspace.contains_window(window.hwnd);
|
||||
let monocle_container = workspace.monocle_container().clone();
|
||||
|
||||
if !workspace_contains_window && !needs_reconciliation {
|
||||
match behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
workspace.new_container_for_window(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
WindowContainerBehaviour::Append => {
|
||||
workspace
|
||||
.focused_container_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused container"))?
|
||||
.add_window(window);
|
||||
self.update_focused_workspace(true, false)?;
|
||||
|
||||
stackbar_manager::send_notification();
|
||||
if (self.focused_workspace()?.containers().len() == 1
|
||||
&& self.focused_workspace()?.floating_windows().is_empty())
|
||||
|| (self.focused_workspace()?.containers().is_empty()
|
||||
&& self.focused_workspace()?.floating_windows().len() == 1)
|
||||
{
|
||||
// If after adding this window the workspace only contains 1 window, it
|
||||
// means it was previously empty and we focused the desktop to unfocus
|
||||
// any previous window from other workspace, so now we need to focus
|
||||
// this window again. This is needed because sometimes some windows
|
||||
// first send the `FocusChange` event and only the `Show` event after
|
||||
// and we will be focusing the desktop on the `FocusChange` event since
|
||||
// it is still empty.
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if workspace_contains_window {
|
||||
let mut monocle_window_event = false;
|
||||
if let Some(ref monocle) = monocle_container {
|
||||
if let Some(monocle_window) = monocle.focused_window() {
|
||||
if monocle_window.hwnd == window.hwnd {
|
||||
monocle_window_event = true;
|
||||
if workspace_contains_window {
|
||||
let mut monocle_window_event = false;
|
||||
if let Some(ref monocle) = monocle_container {
|
||||
if let Some(monocle_window) = monocle.focused_window() {
|
||||
if monocle_window.hwnd == window.hwnd {
|
||||
monocle_window_event = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !monocle_window_event && monocle_container.is_some() {
|
||||
window.hide();
|
||||
if !monocle_window_event && monocle_container.is_some() {
|
||||
window.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::MoveResizeStart(_, window) => {
|
||||
if *self.focused_workspace()?.tile() {
|
||||
let monitor_idx = self.focused_monitor_idx();
|
||||
let workspace_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace_idx();
|
||||
let container_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace()
|
||||
.ok_or_else(|| anyhow!("there is no workspace with this idx"))?
|
||||
.focused_container_idx();
|
||||
let monitor_idx = self.focused_monitor_idx();
|
||||
let workspace_idx = self
|
||||
.focused_monitor()
|
||||
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
|
||||
.focused_workspace_idx();
|
||||
|
||||
WindowsApi::bring_window_to_top(window.hwnd)?;
|
||||
WindowsApi::bring_window_to_top(window.hwnd)?;
|
||||
|
||||
self.pending_move_op =
|
||||
Option::from((monitor_idx, workspace_idx, container_idx));
|
||||
}
|
||||
let pending_move_op = Arc::make_mut(&mut self.pending_move_op);
|
||||
*pending_move_op = Option::from((monitor_idx, workspace_idx, window.hwnd));
|
||||
}
|
||||
WindowManagerEvent::MoveResizeEnd(_, window) => {
|
||||
// We need this because if the event ends on a different monitor,
|
||||
// that monitor will already have been focused and updated in the state
|
||||
let pending = self.pending_move_op;
|
||||
let pending = *self.pending_move_op;
|
||||
// Always consume the pending move op whenever this event is handled
|
||||
self.pending_move_op = None;
|
||||
let pending_move_op = Arc::make_mut(&mut self.pending_move_op);
|
||||
*pending_move_op = None;
|
||||
|
||||
// If the window handles don't match then something went wrong and the pending move
|
||||
// is not related to this current move, if so abort this operation.
|
||||
if let Some((_, _, w_hwnd)) = pending {
|
||||
if w_hwnd != window.hwnd {
|
||||
color_eyre::eyre::bail!(
|
||||
"window handles for move operation don't match: {} != {}",
|
||||
w_hwnd,
|
||||
window.hwnd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let target_monitor_idx = self
|
||||
.monitor_idx_from_current_pos()
|
||||
@@ -402,8 +469,8 @@ impl WindowManager {
|
||||
|
||||
let focused_monitor_idx = self.focused_monitor_idx();
|
||||
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
|
||||
let window_container_behaviour =
|
||||
self.window_container_behaviour(focused_monitor_idx, focused_workspace_idx);
|
||||
let window_management_behaviour =
|
||||
self.window_management_behaviour(focused_monitor_idx, focused_workspace_idx);
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
@@ -418,8 +485,21 @@ impl WindowManager {
|
||||
// place across a monitor boundary to an empty workspace
|
||||
.unwrap_or(&Rect::default());
|
||||
|
||||
// This will be true if we have moved to an empty workspace on another monitor
|
||||
let mut moved_across_monitors = old_position == Rect::default();
|
||||
// This will be true if we have moved to another monitor
|
||||
let mut moved_across_monitors = false;
|
||||
|
||||
for (i, monitors) in self.monitors().iter().enumerate() {
|
||||
for workspace in monitors.workspaces() {
|
||||
if workspace.contains_window(window.hwnd) && i != target_monitor_idx {
|
||||
moved_across_monitors = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if moved_across_monitors {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((origin_monitor_idx, origin_workspace_idx, _)) = pending {
|
||||
// If we didn't move to another monitor with an empty workspace, it is
|
||||
// still possible that we moved to another monitor with a populated workspace
|
||||
@@ -440,8 +520,7 @@ impl WindowManager {
|
||||
.get(origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
|
||||
|
||||
let managed_window =
|
||||
origin_workspace.contains_managed_window(window.hwnd);
|
||||
let managed_window = origin_workspace.contains_window(window.hwnd);
|
||||
|
||||
if !managed_window {
|
||||
moved_across_monitors = false;
|
||||
@@ -451,7 +530,9 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
if workspace.contains_managed_window(window.hwnd) || moved_across_monitors {
|
||||
if (*workspace.tile() && workspace.contains_managed_window(window.hwnd))
|
||||
|| moved_across_monitors
|
||||
{
|
||||
let resize = Rect {
|
||||
left: new_position.left - old_position.left,
|
||||
top: new_position.top - old_position.top,
|
||||
@@ -471,11 +552,8 @@ impl WindowManager {
|
||||
tracing::info!("moving with mouse");
|
||||
|
||||
if moved_across_monitors {
|
||||
if let Some((
|
||||
origin_monitor_idx,
|
||||
origin_workspace_idx,
|
||||
origin_container_idx,
|
||||
)) = pending
|
||||
if let Some((origin_monitor_idx, origin_workspace_idx, w_hwnd)) =
|
||||
pending
|
||||
{
|
||||
let target_workspace_idx = self
|
||||
.monitors()
|
||||
@@ -495,34 +573,41 @@ impl WindowManager {
|
||||
// Default to 0 in the case of an empty workspace
|
||||
.unwrap_or(0);
|
||||
|
||||
self.transfer_container(
|
||||
(
|
||||
origin_monitor_idx,
|
||||
origin_workspace_idx,
|
||||
origin_container_idx,
|
||||
),
|
||||
(
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
target_container_idx,
|
||||
),
|
||||
)?;
|
||||
let origin = (origin_monitor_idx, origin_workspace_idx, w_hwnd);
|
||||
let target = (
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
target_container_idx,
|
||||
);
|
||||
self.transfer_window(origin, target)?;
|
||||
|
||||
// We want to make sure both the origin and target monitors are updated,
|
||||
// so that we don't have ghost tiles until we force an interaction on
|
||||
// the origin monitor's focused workspace
|
||||
self.focus_monitor(origin_monitor_idx)?;
|
||||
self.focus_workspace(origin_workspace_idx)?;
|
||||
let origin_monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
|
||||
origin_monitor.focus_workspace(origin_workspace_idx)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
|
||||
self.focus_monitor(target_monitor_idx)?;
|
||||
self.focus_workspace(target_workspace_idx)?;
|
||||
let target_monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
|
||||
target_monitor.focus_workspace(target_workspace_idx)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
|
||||
// Make sure to give focus to the moved window again
|
||||
window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
// Here we handle a simple move on the same monitor which is treated as
|
||||
// a container swap
|
||||
} else if window_management_behaviour.float_override {
|
||||
workspace.floating_windows_mut().push(window);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
} else {
|
||||
match window_container_behaviour {
|
||||
match window_management_behaviour.current_behaviour {
|
||||
WindowContainerBehaviour::Create => {
|
||||
match workspace.container_idx_from_current_point() {
|
||||
Some(target_idx) => {
|
||||
@@ -624,6 +709,20 @@ impl WindowManager {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
for window in workspace.floating_windows() {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
|
||||
if let Some(window) = workspace.maximized_window() {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
|
||||
if let Some(container) = workspace.monocle_container() {
|
||||
for window in container.windows() {
|
||||
known_hwnds.push(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,13 +735,15 @@ impl WindowManager {
|
||||
|
||||
serde_json::to_writer_pretty(&file, &known_hwnds)?;
|
||||
|
||||
let notification = Notification {
|
||||
event: NotificationEvent::WindowManager(event),
|
||||
state: self.as_ref().into(),
|
||||
};
|
||||
notify_subscribers(
|
||||
Notification {
|
||||
event: NotificationEvent::WindowManager(event),
|
||||
state: self.as_ref().into(),
|
||||
},
|
||||
initial_state.has_been_modified(self.as_ref()),
|
||||
)?;
|
||||
|
||||
notify_subscribers(&serde_json::to_string(¬ification)?)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(Some(event.hwnd()));
|
||||
transparency_manager::send_notification();
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn find_orphans(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
|
||||
let reaped_orphans = workspace.reap_orphans()?;
|
||||
if reaped_orphans.0 > 0 || reaped_orphans.1 > 0 {
|
||||
workspace.update(&work_area, offset, window_based_work_area_offset)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
tracing::info!(
|
||||
"reaped {} orphan window(s) and {} orphaned container(s) on monitor: {}, workspace: {}",
|
||||
reaped_orphans.0,
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
use crate::animation::PerAnimationPrefixConfig;
|
||||
use crate::animation::ANIMATION_DURATION_GLOBAL;
|
||||
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_FPS;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
|
||||
use crate::animation::DEFAULT_ANIMATION_FPS;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::ZOrder;
|
||||
use crate::border_manager::IMPLEMENTATION;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::Z_ORDER;
|
||||
use crate::colour::Colour;
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::StackbarLabel;
|
||||
@@ -20,6 +28,7 @@ use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
|
||||
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::theme_manager;
|
||||
use crate::transparency_manager;
|
||||
use crate::window;
|
||||
use crate::window_manager::WindowManager;
|
||||
@@ -27,16 +36,13 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::CrossBoundaryBehaviour;
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_ENABLED;
|
||||
use crate::ANIMATION_FPS;
|
||||
use crate::ANIMATION_STYLE;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
@@ -49,6 +55,8 @@ use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WINDOWS_11;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
|
||||
use crate::asc::ApplicationSpecificConfiguration;
|
||||
use crate::asc::AscApplicationRulesOrSchema;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::core::config_generation::ApplicationConfiguration;
|
||||
use crate::core::config_generation::ApplicationConfigurationGenerator;
|
||||
@@ -67,6 +75,7 @@ use crate::core::OperationBehaviour;
|
||||
use crate::core::Rect;
|
||||
use crate::core::SocketMessage;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowManagementBehaviour;
|
||||
use color_eyre::Result;
|
||||
use crossbeam_channel::Receiver;
|
||||
use hotwatch::EventKind;
|
||||
@@ -94,6 +103,8 @@ pub struct BorderColours {
|
||||
pub stack: Option<Colour>,
|
||||
/// Border colour when the container is in monocle mode
|
||||
pub monocle: Option<Colour>,
|
||||
/// Border colour when the container is in floating mode
|
||||
pub floating: Option<Colour>,
|
||||
/// Border colour when the container is unfocused
|
||||
pub unfocused: Option<Colour>,
|
||||
}
|
||||
@@ -105,13 +116,13 @@ pub struct WorkspaceConfig {
|
||||
/// Layout (default: BSP)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout: Option<DefaultLayout>,
|
||||
/// Custom Layout (default: None)
|
||||
/// END OF LIFE FEATURE: Custom Layout (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_layout: Option<PathBuf>,
|
||||
/// Layout rules (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
|
||||
/// Layout rules (default: None)
|
||||
/// END OF LIFE FEATURE: Custom layout rules (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_layout_rules: Option<HashMap<usize, PathBuf>>,
|
||||
/// Container padding (default: global)
|
||||
@@ -129,6 +140,13 @@ pub struct WorkspaceConfig {
|
||||
/// Apply this monitor's window-based work area offset (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub apply_window_based_work_area_offset: Option<bool>,
|
||||
/// Determine what happens when a new window is opened (default: Create)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
/// Enable or disable float override, which makes it so every new window opens in floating mode
|
||||
/// (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_override: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<&Workspace> for WorkspaceConfig {
|
||||
@@ -181,6 +199,8 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
initial_workspace_rules: None,
|
||||
workspace_rules: None,
|
||||
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
|
||||
window_container_behaviour: *value.window_container_behaviour(),
|
||||
float_override: *value.float_override(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,7 +237,7 @@ impl From<&Monitor> for MonitorConfig {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.30`
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.31`
|
||||
pub struct StaticConfig {
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -234,6 +254,10 @@ pub struct StaticConfig {
|
||||
/// Determine what happens when a new window is opened (default: Create)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
/// Enable or disable float override, which makes it so every new window opens in floating mode
|
||||
/// (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_override: Option<bool>,
|
||||
/// Determine what happens when a window is moved across a monitor boundary (default: Swap)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cross_monitor_move_behaviour: Option<MoveBehaviour>,
|
||||
@@ -243,13 +267,13 @@ pub struct StaticConfig {
|
||||
/// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unmanaged_window_operation_behaviour: Option<OperationBehaviour>,
|
||||
/// Determine focus follows mouse implementation (default: None)
|
||||
/// END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
/// Enable or disable mouse follows focus (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mouse_follows_focus: Option<bool>,
|
||||
/// Path to applications.yaml from komorebi-application-specific-configurations (default: None)
|
||||
/// Path to applications.json from komorebi-application-specific-configurations (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub app_specific_configuration_path: Option<PathBuf>,
|
||||
/// Width of the window border (default: 8)
|
||||
@@ -272,7 +296,7 @@ pub struct StaticConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(alias = "active_window_border_style")]
|
||||
pub border_style: Option<BorderStyle>,
|
||||
/// Active window border z-order (default: System)
|
||||
/// DEPRECATED from v0.1.31: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_z_order: Option<ZOrder>,
|
||||
/// Active window border implementation (default: Komorebi)
|
||||
@@ -304,10 +328,14 @@ pub struct StaticConfig {
|
||||
pub global_work_area_offset: Option<Rect>,
|
||||
/// Individual window floating rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_rules: Option<Vec<MatchingRule>>,
|
||||
#[serde(alias = "float_rules")]
|
||||
pub ignore_rules: Option<Vec<MatchingRule>>,
|
||||
/// Individual window force-manage rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manage_rules: Option<Vec<MatchingRule>>,
|
||||
/// Identify applications which should be managed as floating windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub floating_applications: Option<Vec<MatchingRule>>,
|
||||
/// Identify border overflow applications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_overflow_applications: Option<Vec<MatchingRule>>,
|
||||
@@ -341,16 +369,20 @@ pub struct StaticConfig {
|
||||
/// How long to wait when compensating for slow applications, in milliseconds (default: 20)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slow_application_compensation_time: Option<u64>,
|
||||
/// Komorebi status bar configuration files for multiple instances on different monitors
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
// this option is a little special because it is only consumed by komorebic
|
||||
pub bar_configurations: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AnimationsConfig {
|
||||
/// Enable or disable animations (default: false)
|
||||
enabled: bool,
|
||||
enabled: PerAnimationPrefixConfig<bool>,
|
||||
/// Set the animation duration in ms (default: 250)
|
||||
duration: Option<u64>,
|
||||
duration: Option<PerAnimationPrefixConfig<u64>>,
|
||||
/// Set the animation style (default: Linear)
|
||||
style: Option<AnimationStyle>,
|
||||
style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
|
||||
/// Set the animation FPS (default: 60)
|
||||
fps: Option<u64>,
|
||||
}
|
||||
@@ -359,7 +391,7 @@ pub struct AnimationsConfig {
|
||||
pub enum KomorebiTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
/// Name of the Catppuccin theme
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
/// Border colour when the container contains a single window (default: Blue)
|
||||
single_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
@@ -367,6 +399,8 @@ pub enum KomorebiTheme {
|
||||
stack_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the container is in monocle mode (default: Pink)
|
||||
monocle_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the window is floating (default: Yellow)
|
||||
floating_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the container is unfocused (default: Base)
|
||||
unfocused_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Stackbar focused tab text colour (default: Green)
|
||||
@@ -380,7 +414,7 @@ pub enum KomorebiTheme {
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)
|
||||
name: komorebi_themes::Base16,
|
||||
/// Border colour when the container contains a single window (default: Base0D)
|
||||
single_border: Option<komorebi_themes::Base16Value>,
|
||||
@@ -388,6 +422,8 @@ pub enum KomorebiTheme {
|
||||
stack_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is in monocle mode (default: Base0F)
|
||||
monocle_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the window is floating (default: Base09)
|
||||
floating_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is unfocused (default: Base01)
|
||||
unfocused_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar focused tab text colour (default: Base0B)
|
||||
@@ -402,6 +438,31 @@ pub enum KomorebiTheme {
|
||||
}
|
||||
|
||||
impl StaticConfig {
|
||||
pub fn end_of_life(raw: &str) {
|
||||
let features = vec![
|
||||
"focus_follows_mouse",
|
||||
"custom_layout",
|
||||
"custom_layout_rules",
|
||||
];
|
||||
|
||||
let mut display = false;
|
||||
|
||||
for feature in features {
|
||||
if raw.contains(feature) {
|
||||
if !display {
|
||||
display = true;
|
||||
println!("\n\"{feature}\" is now end-of-life");
|
||||
} else {
|
||||
println!(r#""{feature}" is now end-of-life"#);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if display {
|
||||
println!("\nEnd-of-life features will not receive any further bug fixes or updates; they should not be used\n")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aliases(raw: &str) {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("border", ["active_window_border"]);
|
||||
@@ -409,6 +470,8 @@ impl StaticConfig {
|
||||
map.insert("border_offset", ["active_window_border_offset"]);
|
||||
map.insert("border_colours", ["active_window_border_colours"]);
|
||||
map.insert("border_style", ["active_window_border_style"]);
|
||||
map.insert("applications.json", ["applications.yaml"]);
|
||||
map.insert("ignore_rules", ["float_rules"]);
|
||||
|
||||
let mut display = false;
|
||||
|
||||
@@ -433,7 +496,7 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
pub fn deprecated(raw: &str) {
|
||||
let deprecated_options = ["invisible_borders"];
|
||||
let deprecated_options = ["invisible_borders", "border_z_order"];
|
||||
let deprecated_variants = vec![
|
||||
("Hide", "window_hiding_behaviour", "Cloak"),
|
||||
("Minimize", "window_hiding_behaviour", "Cloak"),
|
||||
@@ -498,6 +561,9 @@ impl From<&WindowManager> for StaticConfig {
|
||||
single: Option::from(Colour::from(border_manager::FOCUSED.load(Ordering::SeqCst))),
|
||||
stack: Option::from(Colour::from(border_manager::STACK.load(Ordering::SeqCst))),
|
||||
monocle: Option::from(Colour::from(border_manager::MONOCLE.load(Ordering::SeqCst))),
|
||||
floating: Option::from(Colour::from(
|
||||
border_manager::FLOATING.load(Ordering::SeqCst),
|
||||
)),
|
||||
unfocused: Option::from(Colour::from(
|
||||
border_manager::UNFOCUSED.load(Ordering::SeqCst),
|
||||
)),
|
||||
@@ -507,7 +573,10 @@ impl From<&WindowManager> for StaticConfig {
|
||||
Self {
|
||||
invisible_borders: None,
|
||||
resize_delta: Option::from(value.resize_delta),
|
||||
window_container_behaviour: Option::from(value.window_container_behaviour),
|
||||
window_container_behaviour: Option::from(
|
||||
value.window_management_behaviour.current_behaviour,
|
||||
),
|
||||
float_override: Option::from(value.window_management_behaviour.float_override),
|
||||
cross_monitor_move_behaviour: Option::from(value.cross_monitor_move_behaviour),
|
||||
cross_boundary_behaviour: Option::from(value.cross_boundary_behaviour),
|
||||
unmanaged_window_operation_behaviour: Option::from(
|
||||
@@ -530,7 +599,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
),
|
||||
transparency_ignore_rules: None,
|
||||
border_style: Option::from(STYLE.load()),
|
||||
border_z_order: Option::from(Z_ORDER.load()),
|
||||
border_z_order: None,
|
||||
border_implementation: Option::from(IMPLEMENTATION.load()),
|
||||
default_workspace_padding: Option::from(
|
||||
DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst),
|
||||
@@ -541,7 +610,8 @@ impl From<&WindowManager> for StaticConfig {
|
||||
monitors: Option::from(monitors),
|
||||
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
|
||||
global_work_area_offset: value.work_area_offset,
|
||||
float_rules: None,
|
||||
ignore_rules: None,
|
||||
floating_applications: None,
|
||||
manage_rules: None,
|
||||
border_overflow_applications: None,
|
||||
tray_and_multi_window_applications: None,
|
||||
@@ -556,6 +626,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
SLOW_APPLICATION_COMPENSATION_TIME.load(Ordering::SeqCst),
|
||||
),
|
||||
slow_application_identifiers: Option::from(SLOW_APPLICATION_IDENTIFIERS.lock().clone()),
|
||||
bar_configurations: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -587,11 +658,43 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
if let Some(animations) = &self.animation {
|
||||
ANIMATION_ENABLED.store(animations.enabled, Ordering::SeqCst);
|
||||
ANIMATION_DURATION.store(animations.duration.unwrap_or(250), Ordering::SeqCst);
|
||||
ANIMATION_FPS.store(animations.fps.unwrap_or(60), Ordering::SeqCst);
|
||||
let mut animation_style = ANIMATION_STYLE.lock();
|
||||
*animation_style = animations.style.unwrap_or(AnimationStyle::Linear);
|
||||
match &animations.enabled {
|
||||
PerAnimationPrefixConfig::Prefix(enabled) => {
|
||||
ANIMATION_ENABLED_PER_ANIMATION.lock().clone_from(enabled);
|
||||
}
|
||||
PerAnimationPrefixConfig::Global(enabled) => {
|
||||
ANIMATION_ENABLED_GLOBAL.store(*enabled, Ordering::SeqCst);
|
||||
ANIMATION_ENABLED_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
}
|
||||
|
||||
match &animations.style {
|
||||
Some(PerAnimationPrefixConfig::Prefix(style)) => {
|
||||
ANIMATION_STYLE_PER_ANIMATION.lock().clone_from(style);
|
||||
}
|
||||
Some(PerAnimationPrefixConfig::Global(style)) => {
|
||||
let mut animation_style = ANIMATION_STYLE_GLOBAL.lock();
|
||||
*animation_style = *style;
|
||||
ANIMATION_STYLE_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
match &animations.duration {
|
||||
Some(PerAnimationPrefixConfig::Prefix(duration)) => {
|
||||
ANIMATION_DURATION_PER_ANIMATION.lock().clone_from(duration);
|
||||
}
|
||||
Some(PerAnimationPrefixConfig::Global(duration)) => {
|
||||
ANIMATION_DURATION_GLOBAL.store(*duration, Ordering::SeqCst);
|
||||
ANIMATION_DURATION_PER_ANIMATION.lock().clear();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
ANIMATION_FPS.store(
|
||||
animations.fps.unwrap_or(DEFAULT_ANIMATION_FPS),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(container) = self.default_container_padding {
|
||||
@@ -622,6 +725,10 @@ impl StaticConfig {
|
||||
border_manager::MONOCLE.store(u32::from(monocle), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(floating) = colours.floating {
|
||||
border_manager::FLOATING.store(u32::from(floating), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(unfocused) = colours.unfocused {
|
||||
border_manager::UNFOCUSED.store(u32::from(unfocused), Ordering::SeqCst);
|
||||
}
|
||||
@@ -649,7 +756,7 @@ impl StaticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
|
||||
transparency_manager::TRANSPARENCY_ENABLED
|
||||
@@ -657,7 +764,7 @@ impl StaticConfig {
|
||||
transparency_manager::TRANSPARENCY_ALPHA
|
||||
.store(self.transparency_alpha.unwrap_or(200), Ordering::SeqCst);
|
||||
|
||||
let mut float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
|
||||
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
let mut manage_identifiers = MANAGE_IDENTIFIERS.lock();
|
||||
let mut tray_and_multi_window_identifiers = TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock();
|
||||
@@ -665,9 +772,14 @@ impl StaticConfig {
|
||||
let mut layered_identifiers = LAYERED_WHITELIST.lock();
|
||||
let mut transparency_blacklist = TRANSPARENCY_BLACKLIST.lock();
|
||||
let mut slow_application_identifiers = SLOW_APPLICATION_IDENTIFIERS.lock();
|
||||
let mut floating_applications = FLOATING_APPLICATIONS.lock();
|
||||
|
||||
if let Some(rules) = &mut self.float_rules {
|
||||
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
|
||||
if let Some(rules) = &mut self.ignore_rules {
|
||||
populate_rules(rules, &mut ignore_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut self.floating_applications {
|
||||
populate_rules(rules, &mut floating_applications, &mut regex_identifiers)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut self.manage_rules {
|
||||
@@ -743,184 +855,144 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
if let Some(theme) = &self.theme {
|
||||
let (
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
) = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name,
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Blue)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stack_border = stack_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let monocle_border = monocle_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Pink)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let unfocused_border = unfocused_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_unfocused_text = stackbar_unfocused_text
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Text)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_background = stackbar_background
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
|
||||
.color32(name.as_theme());
|
||||
|
||||
(
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
KomorebiTheme::Base16 {
|
||||
name,
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0D)
|
||||
.color32(*name);
|
||||
|
||||
let stack_border = stack_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(*name);
|
||||
|
||||
let monocle_border = monocle_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0F)
|
||||
.color32(*name);
|
||||
|
||||
let unfocused_border = unfocused_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_unfocused_text = stackbar_unfocused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base05)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_background = stackbar_background
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(*name);
|
||||
|
||||
(
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
border_manager::FOCUSED.store(u32::from(Colour::from(single_border)), Ordering::SeqCst);
|
||||
border_manager::MONOCLE
|
||||
.store(u32::from(Colour::from(monocle_border)), Ordering::SeqCst);
|
||||
border_manager::STACK.store(u32::from(Colour::from(stack_border)), Ordering::SeqCst);
|
||||
border_manager::UNFOCUSED
|
||||
.store(u32::from(Colour::from(unfocused_border)), Ordering::SeqCst);
|
||||
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_background)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
STACKBAR_FOCUSED_TEXT_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_focused_text)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_unfocused_text)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
theme_manager::send_notification(*theme);
|
||||
}
|
||||
|
||||
if let Some(path) = &self.app_specific_configuration_path {
|
||||
let path = resolve_home_path(path)?;
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let asc = ApplicationConfigurationGenerator::load(&content)?;
|
||||
match path.extension() {
|
||||
None => {}
|
||||
Some(ext) => match ext.to_string_lossy().to_string().as_str() {
|
||||
"yaml" => {
|
||||
tracing::info!("loading applications.yaml from: {}", path.display());
|
||||
let path = resolve_home_path(path)?;
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let asc = ApplicationConfigurationGenerator::load(&content)?;
|
||||
|
||||
for mut entry in asc {
|
||||
if let Some(rules) = &mut entry.float_identifiers {
|
||||
populate_rules(rules, &mut float_identifiers, &mut regex_identifiers)?;
|
||||
}
|
||||
for mut entry in asc {
|
||||
if let Some(rules) = &mut entry.ignore_identifiers {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut ignore_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(ref options) = entry.options {
|
||||
let options = options.clone();
|
||||
for o in options {
|
||||
match o {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut object_name_change_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
if let Some(ref options) = entry.options {
|
||||
let options = options.clone();
|
||||
for o in options {
|
||||
match o {
|
||||
ApplicationOptions::ObjectNameChange => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut object_name_change_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut layered_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut tray_and_multi_window_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut manage_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {} // deprecated
|
||||
}
|
||||
}
|
||||
}
|
||||
ApplicationOptions::Layered => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut layered_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::TrayAndMultiWindow => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut tray_and_multi_window_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::Force => {
|
||||
populate_option(
|
||||
&mut entry,
|
||||
&mut manage_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
ApplicationOptions::BorderOverflow => {} // deprecated
|
||||
}
|
||||
}
|
||||
}
|
||||
"json" => {
|
||||
tracing::info!("loading applications.json from: {}", path.display());
|
||||
let path = resolve_home_path(path)?;
|
||||
let mut asc = ApplicationSpecificConfiguration::load(&path)?;
|
||||
|
||||
for entry in asc.values_mut() {
|
||||
match entry {
|
||||
AscApplicationRulesOrSchema::Schema(_) => {}
|
||||
AscApplicationRulesOrSchema::AscApplicationRules(entry) => {
|
||||
if let Some(rules) = &mut entry.ignore {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut ignore_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.manage {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut manage_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.floating {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut floating_applications,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.transparency_ignore {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut transparency_blacklist,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.tray_and_multi_window {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut tray_and_multi_window_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.layered {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut layered_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.object_name_change {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut object_name_change_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(rules) = &mut entry.slow_application {
|
||||
populate_rules(
|
||||
rules,
|
||||
&mut slow_application_identifiers,
|
||||
&mut regex_identifiers,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -929,7 +1001,34 @@ impl StaticConfig {
|
||||
|
||||
pub fn read(path: &PathBuf) -> Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let value: Self = serde_json::from_str(&content)?;
|
||||
let mut value: Self = serde_json::from_str(&content)?;
|
||||
|
||||
if let Some(path) = &mut value.app_specific_configuration_path {
|
||||
*path = resolve_home_path(&*path)?;
|
||||
}
|
||||
|
||||
if let Some(monitors) = &mut value.monitors {
|
||||
for m in monitors {
|
||||
for w in &mut m.workspaces {
|
||||
if let Some(path) = &mut w.custom_layout {
|
||||
*path = resolve_home_path(&*path)?;
|
||||
}
|
||||
|
||||
if let Some(map) = &mut w.custom_layout_rules {
|
||||
for path in map.values_mut() {
|
||||
*path = resolve_home_path(&*path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(bar_configurations) = &mut value.bar_configurations {
|
||||
for path in bar_configurations {
|
||||
*path = resolve_home_path(&*path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
@@ -939,8 +1038,7 @@ impl StaticConfig {
|
||||
incoming: Receiver<WindowManagerEvent>,
|
||||
unix_listener: Option<UnixListener>,
|
||||
) -> Result<WindowManager> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let mut value: Self = serde_json::from_str(&content)?;
|
||||
let mut value = Self::read(path)?;
|
||||
value.apply_globals()?;
|
||||
|
||||
let listener = match unix_listener {
|
||||
@@ -970,9 +1068,12 @@ impl StaticConfig {
|
||||
is_paused: false,
|
||||
virtual_desktop_id: current_virtual_desktop(),
|
||||
work_area_offset: value.global_work_area_offset,
|
||||
window_container_behaviour: value
|
||||
.window_container_behaviour
|
||||
.unwrap_or(WindowContainerBehaviour::Create),
|
||||
window_management_behaviour: WindowManagementBehaviour {
|
||||
current_behaviour: value
|
||||
.window_container_behaviour
|
||||
.unwrap_or(WindowContainerBehaviour::Create),
|
||||
float_override: value.float_override.unwrap_or_default(),
|
||||
},
|
||||
cross_monitor_move_behaviour: value
|
||||
.cross_monitor_move_behaviour
|
||||
.unwrap_or(MoveBehaviour::Swap),
|
||||
@@ -987,8 +1088,9 @@ impl StaticConfig {
|
||||
mouse_follows_focus: value.mouse_follows_focus.unwrap_or(true),
|
||||
hotwatch: Hotwatch::new()?,
|
||||
has_pending_raise_op: false,
|
||||
pending_move_op: None,
|
||||
pending_move_op: Arc::new(None),
|
||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||
uncloack_to_ignore: 0,
|
||||
};
|
||||
|
||||
match value.focus_follows_mouse {
|
||||
@@ -1019,8 +1121,7 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
pub fn postload(path: &PathBuf, wm: &Arc<Mutex<WindowManager>>) -> Result<()> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let value: Self = serde_json::from_str(&content)?;
|
||||
let value = Self::read(path)?;
|
||||
let mut wm = wm.lock();
|
||||
|
||||
if let Some(monitors) = value.monitors {
|
||||
@@ -1041,12 +1142,9 @@ impl StaticConfig {
|
||||
);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
ws.load_static_config(
|
||||
monitor
|
||||
.workspaces
|
||||
.get(j)
|
||||
.expect("no static workspace config"),
|
||||
)?;
|
||||
if let Some(workspace_config) = monitor.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1087,8 +1185,7 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
pub fn reload(path: &PathBuf, wm: &mut WindowManager) -> Result<()> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let mut value: Self = serde_json::from_str(&content)?;
|
||||
let mut value = Self::read(path)?;
|
||||
|
||||
value.apply_globals()?;
|
||||
|
||||
@@ -1105,12 +1202,9 @@ impl StaticConfig {
|
||||
);
|
||||
|
||||
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
|
||||
ws.load_static_config(
|
||||
monitor
|
||||
.workspaces
|
||||
.get(j)
|
||||
.expect("no static workspace config"),
|
||||
)?;
|
||||
if let Some(workspace_config) = monitor.workspaces.get(j) {
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1149,7 +1243,11 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
if let Some(val) = value.window_container_behaviour {
|
||||
wm.window_container_behaviour = val;
|
||||
wm.window_management_behaviour.current_behaviour = val;
|
||||
}
|
||||
|
||||
if let Some(val) = value.float_override {
|
||||
wm.window_management_behaviour.float_override = val;
|
||||
}
|
||||
|
||||
if let Some(val) = value.cross_monitor_move_behaviour {
|
||||
|
||||
224
komorebi/src/theme_manager.rs
Normal file
224
komorebi/src/theme_manager.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::stackbar_manager;
|
||||
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::Colour;
|
||||
use crate::KomorebiTheme;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub struct Notification(KomorebiTheme);
|
||||
|
||||
pub static CURRENT_THEME: AtomicCell<Option<KomorebiTheme>> = AtomicCell::new(None);
|
||||
|
||||
impl Deref for Notification {
|
||||
type Target = KomorebiTheme;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
|
||||
|
||||
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
|
||||
CHANNEL.get_or_init(|| crossbeam_channel::bounded(20))
|
||||
}
|
||||
|
||||
fn event_tx() -> Sender<Notification> {
|
||||
channel().0.clone()
|
||||
}
|
||||
|
||||
fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
// Currently this should only be used for async focus updates, such as
|
||||
// when an animation finishes and we need to focus to set the cursor
|
||||
// position if the user has mouse follows focus enabled
|
||||
pub fn send_notification(theme: KomorebiTheme) {
|
||||
if event_tx().try_send(Notification(theme)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications() {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications() {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
tracing::info!("listening");
|
||||
|
||||
let receiver = event_rx();
|
||||
|
||||
for notification in receiver {
|
||||
let theme = ¬ification.0;
|
||||
|
||||
let (
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
) = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name,
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Blue)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stack_border = stack_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let monocle_border = monocle_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Pink)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let floating_border = floating_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Yellow)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let unfocused_border = unfocused_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_unfocused_text = stackbar_unfocused_text
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Text)
|
||||
.color32(name.as_theme());
|
||||
|
||||
let stackbar_background = stackbar_background
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
|
||||
.color32(name.as_theme());
|
||||
|
||||
(
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
KomorebiTheme::Base16 {
|
||||
name,
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0D)
|
||||
.color32(*name);
|
||||
|
||||
let stack_border = stack_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(*name);
|
||||
|
||||
let monocle_border = monocle_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0F)
|
||||
.color32(*name);
|
||||
|
||||
let unfocused_border = unfocused_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(*name);
|
||||
|
||||
let floating_border = floating_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base09)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_focused_text = stackbar_focused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0B)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_unfocused_text = stackbar_unfocused_text
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base05)
|
||||
.color32(*name);
|
||||
|
||||
let stackbar_background = stackbar_background
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base01)
|
||||
.color32(*name);
|
||||
|
||||
(
|
||||
single_border,
|
||||
stack_border,
|
||||
monocle_border,
|
||||
floating_border,
|
||||
unfocused_border,
|
||||
stackbar_focused_text,
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
border_manager::FOCUSED.store(u32::from(Colour::from(single_border)), Ordering::SeqCst);
|
||||
border_manager::MONOCLE.store(u32::from(Colour::from(monocle_border)), Ordering::SeqCst);
|
||||
border_manager::STACK.store(u32::from(Colour::from(stack_border)), Ordering::SeqCst);
|
||||
border_manager::FLOATING.store(u32::from(Colour::from(floating_border)), Ordering::SeqCst);
|
||||
border_manager::UNFOCUSED
|
||||
.store(u32::from(Colour::from(unfocused_border)), Ordering::SeqCst);
|
||||
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_background)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
STACKBAR_FOCUSED_TEXT_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_focused_text)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
STACKBAR_UNFOCUSED_TEXT_COLOUR.store(
|
||||
u32::from(Colour::from(stackbar_unfocused_text)),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
CURRENT_THEME.store(Some(notification.0));
|
||||
|
||||
border_manager::send_notification(None);
|
||||
stackbar_manager::send_notification();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
use crate::border_manager;
|
||||
use crate::animation::lerp::Lerp;
|
||||
use crate::animation::prefix::new_animation_key;
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
use crate::animation::AnimationEngine;
|
||||
use crate::animation::RenderDispatcher;
|
||||
use crate::animation::ANIMATION_DURATION_GLOBAL;
|
||||
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_ENABLED_GLOBAL;
|
||||
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
|
||||
use crate::animation::ANIMATION_MANAGER;
|
||||
use crate::animation::ANIMATION_STYLE_GLOBAL;
|
||||
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
|
||||
use crate::com::SetCloak;
|
||||
use crate::focus_manager;
|
||||
use crate::stackbar_manager;
|
||||
use crate::windows_api;
|
||||
use crate::ANIMATIONS_IN_PROGRESS;
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_ENABLED;
|
||||
use crate::AnimationStyle;
|
||||
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
|
||||
use crate::SLOW_APPLICATION_IDENTIFIERS;
|
||||
use std::collections::HashMap;
|
||||
@@ -15,6 +24,7 @@ use std::fmt::Formatter;
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::core::config_generation::IdWithIdentifier;
|
||||
@@ -35,15 +45,15 @@ use crate::core::ApplicationIdentifier;
|
||||
use crate::core::HidingBehaviour;
|
||||
use crate::core::Rect;
|
||||
|
||||
use crate::animation::Animation;
|
||||
use crate::styles::ExtendedWindowStyle;
|
||||
use crate::styles::WindowStyle;
|
||||
use crate::transparency_manager;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::HIDDEN_HWNDS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::NO_TITLEBAR;
|
||||
@@ -57,16 +67,11 @@ pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0);
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct Window {
|
||||
pub hwnd: isize,
|
||||
#[serde(skip)]
|
||||
animation: Animation,
|
||||
}
|
||||
|
||||
impl From<isize> for Window {
|
||||
fn from(value: isize) -> Self {
|
||||
Self {
|
||||
hwnd: value,
|
||||
animation: Animation::new(value),
|
||||
}
|
||||
Self { hwnd: value }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +79,6 @@ impl From<HWND> for Window {
|
||||
fn from(value: HWND) -> Self {
|
||||
Self {
|
||||
hwnd: value.0 as isize,
|
||||
animation: Animation::new(value.0 as isize),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,11 +158,216 @@ impl Serialize for Window {
|
||||
}
|
||||
}
|
||||
|
||||
struct MovementRenderDispatcher {
|
||||
hwnd: isize,
|
||||
start_rect: Rect,
|
||||
target_rect: Rect,
|
||||
top: bool,
|
||||
style: AnimationStyle,
|
||||
}
|
||||
|
||||
impl MovementRenderDispatcher {
|
||||
const PREFIX: AnimationPrefix = AnimationPrefix::Movement;
|
||||
|
||||
pub fn new(
|
||||
hwnd: isize,
|
||||
start_rect: Rect,
|
||||
target_rect: Rect,
|
||||
top: bool,
|
||||
style: AnimationStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
hwnd,
|
||||
start_rect,
|
||||
target_rect,
|
||||
top,
|
||||
style,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderDispatcher for MovementRenderDispatcher {
|
||||
fn get_animation_key(&self) -> String {
|
||||
new_animation_key(MovementRenderDispatcher::PREFIX, self.hwnd.to_string())
|
||||
}
|
||||
|
||||
fn pre_render(&self) -> Result<()> {
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, progress: f64) -> Result<()> {
|
||||
let new_rect = self.start_rect.lerp(self.target_rect, progress, self.style);
|
||||
|
||||
// using MoveWindow because it runs faster than SetWindowPos
|
||||
// so animation have more fps and feel smoother
|
||||
WindowsApi::move_window(self.hwnd, &new_rect, false)?;
|
||||
WindowsApi::invalidate_rect(self.hwnd, None, false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_render(&self) -> Result<()> {
|
||||
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top)?;
|
||||
if ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX)
|
||||
== 0
|
||||
{
|
||||
if WindowsApi::foreground_window().unwrap_or_default() == self.hwnd {
|
||||
focus_manager::send_notification(self.hwnd)
|
||||
}
|
||||
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
|
||||
stackbar_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct TransparencyRenderDispatcher {
|
||||
hwnd: isize,
|
||||
start_opacity: u8,
|
||||
target_opacity: u8,
|
||||
style: AnimationStyle,
|
||||
is_opaque: bool,
|
||||
}
|
||||
|
||||
impl TransparencyRenderDispatcher {
|
||||
const PREFIX: AnimationPrefix = AnimationPrefix::Transparency;
|
||||
|
||||
pub fn new(
|
||||
hwnd: isize,
|
||||
is_opaque: bool,
|
||||
start_opacity: u8,
|
||||
target_opacity: u8,
|
||||
style: AnimationStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
hwnd,
|
||||
start_opacity,
|
||||
target_opacity,
|
||||
style,
|
||||
is_opaque,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderDispatcher for TransparencyRenderDispatcher {
|
||||
fn get_animation_key(&self) -> String {
|
||||
new_animation_key(TransparencyRenderDispatcher::PREFIX, self.hwnd.to_string())
|
||||
}
|
||||
|
||||
fn pre_render(&self) -> Result<()> {
|
||||
//transparent
|
||||
if !self.is_opaque {
|
||||
let window = Window::from(self.hwnd);
|
||||
let mut ex_style = window.ex_style()?;
|
||||
ex_style.insert(ExtendedWindowStyle::LAYERED);
|
||||
window.update_ex_style(&ex_style)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, progress: f64) -> Result<()> {
|
||||
WindowsApi::set_transparent(
|
||||
self.hwnd,
|
||||
self.start_opacity
|
||||
.lerp(self.target_opacity, progress, self.style),
|
||||
)
|
||||
}
|
||||
|
||||
fn post_render(&self) -> Result<()> {
|
||||
//opaque
|
||||
if self.is_opaque {
|
||||
let window = Window::from(self.hwnd);
|
||||
let mut ex_style = window.ex_style()?;
|
||||
ex_style.remove(ExtendedWindowStyle::LAYERED);
|
||||
window.update_ex_style(&ex_style)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub const fn hwnd(self) -> HWND {
|
||||
HWND(windows_api::as_ptr!(self.hwnd))
|
||||
}
|
||||
|
||||
pub fn move_to_area(&mut self, current_area: &Rect, target_area: &Rect) -> Result<()> {
|
||||
let current_rect = WindowsApi::window_rect(self.hwnd)?;
|
||||
let x_diff = target_area.left - current_area.left;
|
||||
let y_diff = target_area.top - current_area.top;
|
||||
let x_ratio = f32::abs((target_area.right as f32) / (current_area.right as f32));
|
||||
let y_ratio = f32::abs((target_area.bottom as f32) / (current_area.bottom as f32));
|
||||
let window_relative_x = current_rect.left - current_area.left;
|
||||
let window_relative_y = current_rect.top - current_area.top;
|
||||
let corrected_relative_x = (window_relative_x as f32 * x_ratio) as i32;
|
||||
let corrected_relative_y = (window_relative_y as f32 * y_ratio) as i32;
|
||||
let window_x = current_area.left + corrected_relative_x;
|
||||
let window_y = current_area.top + corrected_relative_y;
|
||||
let left = x_diff + window_x;
|
||||
let top = y_diff + window_y;
|
||||
|
||||
let corrected_width = (current_rect.right as f32 * x_ratio) as i32;
|
||||
let corrected_height = (current_rect.bottom as f32 * y_ratio) as i32;
|
||||
|
||||
let new_rect = Rect {
|
||||
left,
|
||||
top,
|
||||
right: corrected_width,
|
||||
bottom: corrected_height,
|
||||
};
|
||||
|
||||
let is_maximized = &new_rect == target_area;
|
||||
if is_maximized {
|
||||
windows_api::WindowsApi::unmaximize_window(self.hwnd);
|
||||
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
|
||||
let move_enabled = animation_enabled
|
||||
.get(&MovementRenderDispatcher::PREFIX)
|
||||
.is_some_and(|v| *v);
|
||||
drop(animation_enabled);
|
||||
|
||||
if move_enabled || ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst) {
|
||||
let anim_count = ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX);
|
||||
self.set_position(&new_rect, true)?;
|
||||
let hwnd = self.hwnd;
|
||||
// Wait for the animation to finish before maximizing the window again, otherwise
|
||||
// we would be maximizing the window on the current monitor anyway
|
||||
thread::spawn(move || {
|
||||
let mut new_anim_count = ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX);
|
||||
let mut max_wait = 2000; // Max waiting time. No one will be using an animation longer than 2s, right? RIGHT??? WHY?
|
||||
while new_anim_count > anim_count && max_wait > 0 {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
new_anim_count = ANIMATION_MANAGER
|
||||
.lock()
|
||||
.count_in_progress(MovementRenderDispatcher::PREFIX);
|
||||
max_wait -= 1;
|
||||
}
|
||||
windows_api::WindowsApi::maximize_window(hwnd);
|
||||
});
|
||||
} else {
|
||||
self.set_position(&new_rect, true)?;
|
||||
windows_api::WindowsApi::maximize_window(self.hwnd);
|
||||
}
|
||||
} else {
|
||||
self.set_position(&new_rect, true)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
|
||||
let half_width = work_area.right / 2;
|
||||
let half_weight = work_area.bottom / 2;
|
||||
@@ -174,53 +383,6 @@ impl Window {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn animate_position(&self, start_rect: &Rect, target_rect: &Rect, top: bool) -> Result<()> {
|
||||
let start_rect = *start_rect;
|
||||
let target_rect = *target_rect;
|
||||
let duration = Duration::from_millis(ANIMATION_DURATION.load(Ordering::SeqCst));
|
||||
let mut animation = self.animation;
|
||||
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
border_manager::send_notification();
|
||||
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
stackbar_manager::send_notification();
|
||||
|
||||
let hwnd = self.hwnd;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
animation.animate(duration, |progress: f64| {
|
||||
let new_rect = Animation::lerp_rect(&start_rect, &target_rect, progress);
|
||||
|
||||
if progress == 1.0 {
|
||||
WindowsApi::position_window(hwnd, &new_rect, top)?;
|
||||
if WindowsApi::foreground_window().unwrap_or_default() == hwnd {
|
||||
focus_manager::send_notification(hwnd)
|
||||
}
|
||||
|
||||
if ANIMATIONS_IN_PROGRESS.load(Ordering::Acquire) == 0 {
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED
|
||||
.store(false, Ordering::SeqCst);
|
||||
|
||||
border_manager::send_notification();
|
||||
stackbar_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
}
|
||||
} else {
|
||||
// using MoveWindow because it runs faster than SetWindowPos
|
||||
// so animation have more fps and feel smoother
|
||||
WindowsApi::move_window(hwnd, &new_rect, false)?;
|
||||
WindowsApi::invalidate_rect(hwnd, None, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_position(&self, layout: &Rect, top: bool) -> Result<()> {
|
||||
let window_rect = WindowsApi::window_rect(self.hwnd)?;
|
||||
|
||||
@@ -228,8 +390,27 @@ impl Window {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if ANIMATION_ENABLED.load(Ordering::SeqCst) {
|
||||
self.animate_position(&window_rect, layout, top)
|
||||
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
|
||||
let move_enabled = animation_enabled.get(&MovementRenderDispatcher::PREFIX);
|
||||
|
||||
if move_enabled.is_some_and(|enabled| *enabled)
|
||||
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
|
||||
{
|
||||
let duration = Duration::from_millis(
|
||||
*ANIMATION_DURATION_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&MovementRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
|
||||
);
|
||||
let style = *ANIMATION_STYLE_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&MovementRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
|
||||
|
||||
let render_dispatcher =
|
||||
MovementRenderDispatcher::new(self.hwnd, window_rect, *layout, top, style);
|
||||
|
||||
AnimationEngine::animate(render_dispatcher, duration)
|
||||
} else {
|
||||
WindowsApi::position_window(self.hwnd, layout, top)
|
||||
}
|
||||
@@ -280,7 +461,10 @@ impl Window {
|
||||
}
|
||||
|
||||
pub fn minimize(self) {
|
||||
WindowsApi::minimize_window(self.hwnd);
|
||||
let exe = self.exe().unwrap_or_default();
|
||||
if !exe.contains("komorebi-bar") {
|
||||
WindowsApi::minimize_window(self.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(self) -> Result<()> {
|
||||
@@ -334,20 +518,81 @@ impl Window {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_focused(self) -> bool {
|
||||
WindowsApi::foreground_window().unwrap_or_default() == self.hwnd
|
||||
}
|
||||
|
||||
pub fn transparent(self) -> Result<()> {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.insert(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)?;
|
||||
WindowsApi::set_transparent(
|
||||
self.hwnd,
|
||||
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
|
||||
)
|
||||
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
|
||||
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
|
||||
|
||||
if transparent_enabled.is_some_and(|enabled| *enabled)
|
||||
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
|
||||
{
|
||||
let duration = Duration::from_millis(
|
||||
*ANIMATION_DURATION_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&TransparencyRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
|
||||
);
|
||||
let style = *ANIMATION_STYLE_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&TransparencyRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
|
||||
|
||||
let render_dispatcher = TransparencyRenderDispatcher::new(
|
||||
self.hwnd,
|
||||
false,
|
||||
WindowsApi::get_transparent(self.hwnd).unwrap_or(255),
|
||||
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
|
||||
style,
|
||||
);
|
||||
|
||||
AnimationEngine::animate(render_dispatcher, duration)
|
||||
} else {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.insert(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)?;
|
||||
WindowsApi::set_transparent(
|
||||
self.hwnd,
|
||||
transparency_manager::TRANSPARENCY_ALPHA.load_consume(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn opaque(self) -> Result<()> {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.remove(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)
|
||||
let animation_enabled = ANIMATION_ENABLED_PER_ANIMATION.lock();
|
||||
let transparent_enabled = animation_enabled.get(&TransparencyRenderDispatcher::PREFIX);
|
||||
|
||||
if transparent_enabled.is_some_and(|enabled| *enabled)
|
||||
|| ANIMATION_ENABLED_GLOBAL.load(Ordering::SeqCst)
|
||||
{
|
||||
let duration = Duration::from_millis(
|
||||
*ANIMATION_DURATION_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&TransparencyRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_DURATION_GLOBAL.load(Ordering::SeqCst)),
|
||||
);
|
||||
let style = *ANIMATION_STYLE_PER_ANIMATION
|
||||
.lock()
|
||||
.get(&TransparencyRenderDispatcher::PREFIX)
|
||||
.unwrap_or(&ANIMATION_STYLE_GLOBAL.lock());
|
||||
|
||||
let render_dispatcher = TransparencyRenderDispatcher::new(
|
||||
self.hwnd,
|
||||
true,
|
||||
WindowsApi::get_transparent(self.hwnd)
|
||||
.unwrap_or(transparency_manager::TRANSPARENCY_ALPHA.load_consume()),
|
||||
255,
|
||||
style,
|
||||
);
|
||||
|
||||
AnimationEngine::animate(render_dispatcher, duration)
|
||||
} else {
|
||||
let mut ex_style = self.ex_style()?;
|
||||
ex_style.remove(ExtendedWindowStyle::LAYERED);
|
||||
self.update_ex_style(&ex_style)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_accent(self, colour: u32) -> Result<()> {
|
||||
@@ -534,9 +779,10 @@ pub struct RuleDebug {
|
||||
pub class: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub matches_permaignore_class: Option<String>,
|
||||
pub matches_float_identifier: Option<MatchingRule>,
|
||||
pub matches_ignore_identifier: Option<MatchingRule>,
|
||||
pub matches_managed_override: Option<MatchingRule>,
|
||||
pub matches_layered_whitelist: Option<MatchingRule>,
|
||||
pub matches_floating_applications: Option<MatchingRule>,
|
||||
pub matches_wsl2_gui: Option<String>,
|
||||
pub matches_no_titlebar: Option<String>,
|
||||
}
|
||||
@@ -563,16 +809,16 @@ fn window_is_eligible(
|
||||
|
||||
let regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
|
||||
let float_identifiers = FLOAT_IDENTIFIERS.lock();
|
||||
let should_float = if let Some(rule) = should_act(
|
||||
let ignore_identifiers = IGNORE_IDENTIFIERS.lock();
|
||||
let should_ignore = if let Some(rule) = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&float_identifiers,
|
||||
&ignore_identifiers,
|
||||
®ex_identifiers,
|
||||
) {
|
||||
debug.matches_float_identifier = Some(rule);
|
||||
debug.matches_ignore_identifier = Some(rule);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -593,7 +839,19 @@ fn window_is_eligible(
|
||||
false
|
||||
};
|
||||
|
||||
if should_float && !managed_override {
|
||||
let floating_identifiers = FLOATING_APPLICATIONS.lock();
|
||||
if let Some(rule) = should_act(
|
||||
title,
|
||||
exe_name,
|
||||
class,
|
||||
path,
|
||||
&floating_identifiers,
|
||||
®ex_identifiers,
|
||||
) {
|
||||
debug.matches_floating_applications = Some(rule);
|
||||
}
|
||||
|
||||
if should_ignore && !managed_override {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -735,10 +993,7 @@ pub fn should_act_individual(
|
||||
let mut should_act = false;
|
||||
|
||||
match identifier.matching_strategy {
|
||||
None => {
|
||||
panic!("there is no matching strategy identified for this rule");
|
||||
}
|
||||
Some(MatchingStrategy::Legacy) => match identifier.kind {
|
||||
None | Some(MatchingStrategy::Legacy) => match identifier.kind {
|
||||
ApplicationIdentifier::Title => {
|
||||
if title.starts_with(&identifier.id) || title.ends_with(&identifier.id) {
|
||||
should_act = true;
|
||||
|
||||
@@ -39,6 +39,7 @@ use crate::core::Rect;
|
||||
use crate::core::Sizing;
|
||||
use crate::core::StackbarLabel;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowManagementBehaviour;
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::STYLE;
|
||||
@@ -71,9 +72,9 @@ use crate::Rgb;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::HOME_DIR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
@@ -92,7 +93,7 @@ pub struct WindowManager {
|
||||
pub is_paused: bool,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub resize_delta: i32,
|
||||
pub window_container_behaviour: WindowContainerBehaviour,
|
||||
pub window_management_behaviour: WindowManagementBehaviour,
|
||||
pub cross_monitor_move_behaviour: MoveBehaviour,
|
||||
pub cross_boundary_behaviour: CrossBoundaryBehaviour,
|
||||
pub unmanaged_window_operation_behaviour: OperationBehaviour,
|
||||
@@ -101,8 +102,9 @@ pub struct WindowManager {
|
||||
pub hotwatch: Hotwatch,
|
||||
pub virtual_desktop_id: Option<Vec<u8>>,
|
||||
pub has_pending_raise_op: bool,
|
||||
pub pending_move_op: Option<(usize, usize, usize)>,
|
||||
pub pending_move_op: Arc<Option<(usize, usize, isize)>>,
|
||||
pub already_moved_window_handles: Arc<Mutex<HashSet<isize>>>,
|
||||
pub uncloack_to_ignore: usize,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
@@ -112,6 +114,7 @@ pub struct State {
|
||||
pub is_paused: bool,
|
||||
pub resize_delta: i32,
|
||||
pub new_window_behaviour: WindowContainerBehaviour,
|
||||
pub float_override: bool,
|
||||
pub cross_monitor_move_behaviour: MoveBehaviour,
|
||||
pub unmanaged_window_operation_behaviour: OperationBehaviour,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
@@ -120,6 +123,54 @@ pub struct State {
|
||||
pub has_pending_raise_op: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn has_been_modified(&self, wm: &WindowManager) -> bool {
|
||||
let new = Self::from(wm);
|
||||
|
||||
if self.monitors != new.monitors {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.is_paused != new.is_paused {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.new_window_behaviour != new.new_window_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.float_override != new.float_override {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.cross_monitor_move_behaviour != new.cross_monitor_move_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.unmanaged_window_operation_behaviour != new.unmanaged_window_operation_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.work_area_offset != new.work_area_offset {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.focus_follows_mouse != new.focus_follows_mouse {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.mouse_follows_focus != new.mouse_follows_focus {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.has_pending_raise_op != new.has_pending_raise_op {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GlobalState {
|
||||
@@ -136,7 +187,8 @@ pub struct GlobalState {
|
||||
pub stackbar_tab_width: i32,
|
||||
pub stackbar_height: i32,
|
||||
pub remove_titlebars: bool,
|
||||
pub float_identifiers: Vec<MatchingRule>,
|
||||
#[serde(alias = "float_identifiers")]
|
||||
pub ignore_identifiers: Vec<MatchingRule>,
|
||||
pub manage_identifiers: Vec<MatchingRule>,
|
||||
pub layered_whitelist: Vec<MatchingRule>,
|
||||
pub tray_and_multi_window_identifiers: Vec<MatchingRule>,
|
||||
@@ -164,6 +216,9 @@ impl Default for GlobalState {
|
||||
monocle: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::MONOCLE.load(Ordering::SeqCst),
|
||||
))),
|
||||
floating: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::FLOATING.load(Ordering::SeqCst),
|
||||
))),
|
||||
unfocused: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::UNFOCUSED.load(Ordering::SeqCst),
|
||||
))),
|
||||
@@ -185,7 +240,7 @@ impl Default for GlobalState {
|
||||
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
|
||||
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
|
||||
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
|
||||
float_identifiers: FLOAT_IDENTIFIERS.lock().clone(),
|
||||
ignore_identifiers: IGNORE_IDENTIFIERS.lock().clone(),
|
||||
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
|
||||
layered_whitelist: LAYERED_WHITELIST.lock().clone(),
|
||||
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
|
||||
@@ -214,7 +269,8 @@ impl From<&WindowManager> for State {
|
||||
is_paused: wm.is_paused,
|
||||
work_area_offset: wm.work_area_offset,
|
||||
resize_delta: wm.resize_delta,
|
||||
new_window_behaviour: wm.window_container_behaviour,
|
||||
new_window_behaviour: wm.window_management_behaviour.current_behaviour,
|
||||
float_override: wm.window_management_behaviour.float_override,
|
||||
cross_monitor_move_behaviour: wm.cross_monitor_move_behaviour,
|
||||
focus_follows_mouse: wm.focus_follows_mouse,
|
||||
mouse_follows_focus: wm.mouse_follows_focus,
|
||||
@@ -233,6 +289,7 @@ struct EnforceWorkspaceRuleOp {
|
||||
origin_workspace_idx: usize,
|
||||
target_monitor_idx: usize,
|
||||
target_workspace_idx: usize,
|
||||
floating: bool,
|
||||
}
|
||||
impl EnforceWorkspaceRuleOp {
|
||||
const fn is_origin(&self, monitor_idx: usize, workspace_idx: usize) -> bool {
|
||||
@@ -274,7 +331,7 @@ impl WindowManager {
|
||||
is_paused: false,
|
||||
virtual_desktop_id: current_virtual_desktop(),
|
||||
work_area_offset: None,
|
||||
window_container_behaviour: WindowContainerBehaviour::Create,
|
||||
window_management_behaviour: WindowManagementBehaviour::default(),
|
||||
cross_monitor_move_behaviour: MoveBehaviour::Swap,
|
||||
cross_boundary_behaviour: CrossBoundaryBehaviour::Workspace,
|
||||
unmanaged_window_operation_behaviour: OperationBehaviour::Op,
|
||||
@@ -283,8 +340,9 @@ impl WindowManager {
|
||||
mouse_follows_focus: true,
|
||||
hotwatch: Hotwatch::new()?,
|
||||
has_pending_raise_op: false,
|
||||
pending_move_op: None,
|
||||
pending_move_op: Arc::new(None),
|
||||
already_moved_window_handles: Arc::new(Mutex::new(HashSet::new())),
|
||||
uncloack_to_ignore: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -307,22 +365,52 @@ impl WindowManager {
|
||||
StaticConfig::reload(pathbuf, self)
|
||||
}
|
||||
|
||||
pub fn window_container_behaviour(
|
||||
pub fn window_management_behaviour(
|
||||
&self,
|
||||
monitor_idx: usize,
|
||||
workspace_idx: usize,
|
||||
) -> WindowContainerBehaviour {
|
||||
) -> WindowManagementBehaviour {
|
||||
if let Some(monitor) = self.monitors().get(monitor_idx) {
|
||||
if let Some(workspace) = monitor.workspaces().get(workspace_idx) {
|
||||
return if workspace.containers().is_empty() {
|
||||
WindowContainerBehaviour::Create
|
||||
let current_behaviour =
|
||||
if let Some(behaviour) = workspace.window_container_behaviour() {
|
||||
if workspace.containers().is_empty()
|
||||
&& matches!(behaviour, WindowContainerBehaviour::Append)
|
||||
{
|
||||
// You can't append to an empty workspace
|
||||
WindowContainerBehaviour::Create
|
||||
} else {
|
||||
*behaviour
|
||||
}
|
||||
} else if workspace.containers().is_empty()
|
||||
&& matches!(
|
||||
self.window_management_behaviour.current_behaviour,
|
||||
WindowContainerBehaviour::Append
|
||||
)
|
||||
{
|
||||
// You can't append to an empty workspace
|
||||
WindowContainerBehaviour::Create
|
||||
} else {
|
||||
self.window_management_behaviour.current_behaviour
|
||||
};
|
||||
|
||||
let float_override = if let Some(float_override) = workspace.float_override() {
|
||||
*float_override
|
||||
} else {
|
||||
self.window_container_behaviour
|
||||
self.window_management_behaviour.float_override
|
||||
};
|
||||
|
||||
return WindowManagementBehaviour {
|
||||
current_behaviour,
|
||||
float_override,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
WindowContainerBehaviour::Create
|
||||
WindowManagementBehaviour {
|
||||
current_behaviour: WindowContainerBehaviour::Create,
|
||||
float_override: self.window_management_behaviour.float_override,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -410,6 +498,35 @@ impl WindowManager {
|
||||
None
|
||||
}
|
||||
|
||||
/// Calculates the direction of a move across monitors given a specific monitor index
|
||||
pub fn direction_from_monitor_idx(
|
||||
&self,
|
||||
target_monitor_idx: usize,
|
||||
) -> Option<OperationDirection> {
|
||||
let current_monitor_idx = self.focused_monitor_idx();
|
||||
if current_monitor_idx == target_monitor_idx {
|
||||
return None;
|
||||
}
|
||||
|
||||
let current_monitor_size = self.focused_monitor_size().ok()?;
|
||||
let target_monitor_size = *self.monitors().get(target_monitor_idx)?.size();
|
||||
|
||||
if target_monitor_size.left + target_monitor_size.right == current_monitor_size.left {
|
||||
return Some(OperationDirection::Left);
|
||||
}
|
||||
if current_monitor_size.right + current_monitor_size.left == target_monitor_size.left {
|
||||
return Some(OperationDirection::Right);
|
||||
}
|
||||
if target_monitor_size.top + target_monitor_size.bottom == current_monitor_size.top {
|
||||
return Some(OperationDirection::Up);
|
||||
}
|
||||
if current_monitor_size.top + current_monitor_size.bottom == target_monitor_size.top {
|
||||
return Some(OperationDirection::Down);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
fn add_window_handle_to_move_based_on_workspace_rule(
|
||||
@@ -420,6 +537,7 @@ impl WindowManager {
|
||||
origin_workspace_idx: usize,
|
||||
target_monitor_idx: usize,
|
||||
target_workspace_idx: usize,
|
||||
floating: bool,
|
||||
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
|
||||
) -> () {
|
||||
tracing::trace!(
|
||||
@@ -436,6 +554,7 @@ impl WindowManager {
|
||||
origin_workspace_idx,
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
floating,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -491,6 +610,8 @@ impl WindowManager {
|
||||
};
|
||||
|
||||
if matched {
|
||||
let floating = workspace.floating_windows().contains(window);
|
||||
|
||||
if rule.initial_only {
|
||||
if !already_moved_window_handles.contains(&window.hwnd) {
|
||||
already_moved_window_handles.insert(window.hwnd);
|
||||
@@ -502,6 +623,7 @@ impl WindowManager {
|
||||
j,
|
||||
rule.monitor_index,
|
||||
rule.workspace_index,
|
||||
floating,
|
||||
&mut to_move,
|
||||
);
|
||||
}
|
||||
@@ -513,6 +635,7 @@ impl WindowManager {
|
||||
j,
|
||||
rule.monitor_index,
|
||||
rule.workspace_index,
|
||||
floating,
|
||||
&mut to_move,
|
||||
);
|
||||
}
|
||||
@@ -531,17 +654,34 @@ impl WindowManager {
|
||||
|
||||
// Parse the operation and remove any windows that are not placed according to their rules
|
||||
for op in &to_move {
|
||||
let origin_workspace = self
|
||||
let target_area = *self
|
||||
.monitors_mut()
|
||||
.get_mut(op.target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
|
||||
.work_area_size();
|
||||
|
||||
let origin_monitor = self
|
||||
.monitors_mut()
|
||||
.get_mut(op.origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor with that index"))?
|
||||
.ok_or_else(|| anyhow!("there is no monitor with that index"))?;
|
||||
|
||||
let origin_area = *origin_monitor.work_area_size();
|
||||
|
||||
let origin_workspace = origin_monitor
|
||||
.workspaces_mut()
|
||||
.get_mut(op.origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace with that index"))?;
|
||||
|
||||
let mut window = Window::from(op.hwnd);
|
||||
|
||||
// If it is a floating window move it to the target area
|
||||
if op.floating {
|
||||
window.move_to_area(&origin_area, &target_area)?;
|
||||
}
|
||||
|
||||
// Hide the window we are about to remove if it is on the currently focused workspace
|
||||
if op.is_origin(focused_monitor_idx, focused_workspace_idx) {
|
||||
Window::from(op.hwnd).hide();
|
||||
window.hide();
|
||||
should_update_focused_workspace = true;
|
||||
}
|
||||
|
||||
@@ -571,7 +711,21 @@ impl WindowManager {
|
||||
.get_mut(op.target_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace with that index"))?;
|
||||
|
||||
target_workspace.new_container_for_window(Window::from(op.hwnd));
|
||||
if op.floating {
|
||||
target_workspace
|
||||
.floating_windows_mut()
|
||||
.push(Window::from(op.hwnd));
|
||||
} else {
|
||||
//TODO(alex-ds13): should this take into account the target workspace
|
||||
//`window_container_behaviour`?
|
||||
//In the case above a floating window should always be moved as floating,
|
||||
//because it was set as so either manually by the user or by a
|
||||
//`floating_applications` rule so it should stay that way. But a tiled window
|
||||
//when moving to another workspace by a `workspace_rule` should honor that
|
||||
//workspace `window_container_behaviour` in my opinion! Maybe this should be done
|
||||
//on the `new_container_for_window` function instead.
|
||||
target_workspace.new_container_for_window(Window::from(op.hwnd));
|
||||
}
|
||||
}
|
||||
|
||||
// Only re-tile the focused workspace if we need to
|
||||
@@ -696,6 +850,133 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn transfer_window(
|
||||
&mut self,
|
||||
origin: (usize, usize, isize),
|
||||
target: (usize, usize, usize),
|
||||
) -> Result<()> {
|
||||
let (origin_monitor_idx, origin_workspace_idx, w_hwnd) = origin;
|
||||
let (target_monitor_idx, target_workspace_idx, target_container_idx) = target;
|
||||
|
||||
let origin_workspace = self
|
||||
.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("cannot get monitor idx"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
|
||||
|
||||
let origin_container_idx = origin_workspace
|
||||
.container_for_window(w_hwnd)
|
||||
.and_then(|c| origin_workspace.containers().iter().position(|cc| cc == c));
|
||||
|
||||
if let Some(origin_container_idx) = origin_container_idx {
|
||||
// Moving normal container window
|
||||
self.transfer_container(
|
||||
(
|
||||
origin_monitor_idx,
|
||||
origin_workspace_idx,
|
||||
origin_container_idx,
|
||||
),
|
||||
(
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
target_container_idx,
|
||||
),
|
||||
)?;
|
||||
} else if let Some(idx) = origin_workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.position(|w| w.hwnd == w_hwnd)
|
||||
{
|
||||
// Moving floating window
|
||||
// There is no need to physically move the floating window between areas with
|
||||
// `move_to_area` because the user already did that, so we only need to transfer the
|
||||
// window to the target `floating_windows`
|
||||
let floating_window = origin_workspace.floating_windows_mut().remove(idx);
|
||||
|
||||
let target_workspace = self
|
||||
.monitors_mut()
|
||||
.get_mut(target_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused workspace for this monitor"))?;
|
||||
|
||||
target_workspace
|
||||
.floating_windows_mut()
|
||||
.push(floating_window);
|
||||
} else if origin_workspace
|
||||
.monocle_container()
|
||||
.as_ref()
|
||||
.and_then(|monocle| monocle.focused_window().map(|w| w.hwnd == w_hwnd))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// Moving monocle container
|
||||
if let Some(monocle_idx) = origin_workspace.monocle_container_restore_idx() {
|
||||
let origin_workspace = self
|
||||
.monitors_mut()
|
||||
.get_mut(origin_monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
|
||||
.workspaces_mut()
|
||||
.get_mut(origin_workspace_idx)
|
||||
.ok_or_else(|| anyhow!("there is no workspace for this monitor"))?;
|
||||
let mut uncloack_amount = 0;
|
||||
for container in origin_workspace.containers_mut() {
|
||||
container.restore();
|
||||
uncloack_amount += 1;
|
||||
}
|
||||
origin_workspace.reintegrate_monocle_container()?;
|
||||
|
||||
self.transfer_container(
|
||||
(origin_monitor_idx, origin_workspace_idx, monocle_idx),
|
||||
(
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
target_container_idx,
|
||||
),
|
||||
)?;
|
||||
// After we restore the origin workspace, some windows that were cloacked
|
||||
// by the monocle might now be uncloacked which would trigger a workspace
|
||||
// reconciliation since the focused monitor would be different from origin.
|
||||
// That workspace reconciliation would focus the window on the origin monitor.
|
||||
// So we need to ignore the uncloak events produced by the origin workspace
|
||||
// restore to avoid that issue.
|
||||
self.uncloack_to_ignore = uncloack_amount;
|
||||
}
|
||||
} else if origin_workspace
|
||||
.maximized_window()
|
||||
.as_ref()
|
||||
.map(|max| max.hwnd == w_hwnd)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// Moving maximized_window
|
||||
if let Some(maximized_idx) = origin_workspace.maximized_window_restore_idx() {
|
||||
self.focus_monitor(origin_monitor_idx)?;
|
||||
let origin_monitor = self
|
||||
.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no origin monitor"))?;
|
||||
origin_monitor.focus_workspace(origin_workspace_idx)?;
|
||||
self.unmaximize_window()?;
|
||||
self.focus_monitor(target_monitor_idx)?;
|
||||
let target_monitor = self
|
||||
.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no target monitor"))?;
|
||||
target_monitor.focus_workspace(target_workspace_idx)?;
|
||||
|
||||
self.transfer_container(
|
||||
(origin_monitor_idx, origin_workspace_idx, maximized_idx),
|
||||
(
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
target_container_idx,
|
||||
),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn transfer_container(
|
||||
&mut self,
|
||||
@@ -826,7 +1107,7 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.focused_workspace()?.containers().is_empty() {
|
||||
if self.focused_workspace()?.is_empty() {
|
||||
let desktop_window = Window::from(WindowsApi::desktop_window()?);
|
||||
|
||||
match WindowsApi::raise_and_focus_window(desktop_window.hwnd) {
|
||||
@@ -845,6 +1126,8 @@ impl WindowManager {
|
||||
&& self.focused_workspace()?.maximized_window().is_none()
|
||||
// and we don't have a monocle container
|
||||
&& self.focused_workspace()?.monocle_container().is_none()
|
||||
// and we don't have any floating windows that should show on top
|
||||
&& self.focused_workspace()?.floating_windows().is_empty()
|
||||
{
|
||||
if let Ok(window) = self.focused_window_mut() {
|
||||
if trigger_focus {
|
||||
@@ -1129,6 +1412,7 @@ impl WindowManager {
|
||||
monitor_idx: usize,
|
||||
workspace_idx: Option<usize>,
|
||||
follow: bool,
|
||||
move_direction: Option<OperationDirection>,
|
||||
) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
@@ -1149,6 +1433,8 @@ impl WindowManager {
|
||||
.focused_monitor_mut()
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
let current_area = *monitor.work_area_size();
|
||||
|
||||
let workspace = monitor
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no workspace"))?;
|
||||
@@ -1157,16 +1443,23 @@ impl WindowManager {
|
||||
bail!("cannot move native maximized window to another monitor or workspace");
|
||||
}
|
||||
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
let container_hwnds = container
|
||||
.windows()
|
||||
let foreground_hwnd = WindowsApi::foreground_window()?;
|
||||
let floating_window_index = workspace
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
.position(|w| w.hwnd == foreground_hwnd);
|
||||
|
||||
let floating_window =
|
||||
floating_window_index.map(|idx| workspace.floating_windows_mut().remove(idx));
|
||||
let container = if floating_window_index.is_none() {
|
||||
Some(
|
||||
workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
|
||||
let target_monitor = self
|
||||
@@ -1174,21 +1467,49 @@ impl WindowManager {
|
||||
.get_mut(monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
target_monitor.add_container(container, workspace_idx)?;
|
||||
|
||||
let mut should_load_workspace = false;
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
target_monitor.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
|
||||
if let Some(workspace) = target_monitor.focused_workspace() {
|
||||
if !*workspace.tile() {
|
||||
for hwnd in container_hwnds {
|
||||
Window::from(hwnd).center(target_monitor.work_area_size())?;
|
||||
}
|
||||
if workspace_idx != target_monitor.focused_workspace_idx() {
|
||||
target_monitor.focus_workspace(workspace_idx)?;
|
||||
should_load_workspace = true;
|
||||
}
|
||||
}
|
||||
let target_workspace = target_monitor
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused workspace on target monitor"))?;
|
||||
|
||||
target_monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
if let Some(window) = floating_window {
|
||||
target_workspace.floating_windows_mut().push(window);
|
||||
Window::from(window.hwnd)
|
||||
.move_to_area(¤t_area, target_monitor.work_area_size())?;
|
||||
} else if let Some(container) = container {
|
||||
let container_hwnds = container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(direction) = move_direction {
|
||||
target_monitor.add_container_with_direction(container, workspace_idx, direction)?;
|
||||
} else {
|
||||
target_monitor.add_container(container, workspace_idx)?;
|
||||
}
|
||||
|
||||
if let Some(workspace) = target_monitor.focused_workspace() {
|
||||
if !*workspace.tile() {
|
||||
for hwnd in container_hwnds {
|
||||
Window::from(hwnd)
|
||||
.move_to_area(¤t_area, target_monitor.work_area_size())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("failed to find a window to move");
|
||||
}
|
||||
|
||||
if should_load_workspace {
|
||||
target_monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
}
|
||||
target_monitor.update_focused_workspace(offset)?;
|
||||
|
||||
// this second one is for DPI changes when the target is another monitor
|
||||
@@ -1267,13 +1588,14 @@ impl WindowManager {
|
||||
|
||||
tracing::info!("focusing container");
|
||||
|
||||
let new_idx = if workspace.monocle_container().is_some() {
|
||||
None
|
||||
} else {
|
||||
workspace.new_idx_for_direction(direction)
|
||||
};
|
||||
let new_idx =
|
||||
if workspace.maximized_window().is_some() || workspace.monocle_container().is_some() {
|
||||
None
|
||||
} else {
|
||||
workspace.new_idx_for_direction(direction)
|
||||
};
|
||||
|
||||
let mut cross_monitor_monocle = false;
|
||||
let mut cross_monitor_monocle_or_max = false;
|
||||
|
||||
// this is for when we are scrolling across workspaces like PaperWM
|
||||
if new_idx.is_none()
|
||||
@@ -1350,14 +1672,24 @@ impl WindowManager {
|
||||
let mouse_follows_focus = self.mouse_follows_focus;
|
||||
|
||||
if let Ok(focused_workspace) = self.focused_workspace_mut() {
|
||||
if let Some(monocle) = focused_workspace.monocle_container() {
|
||||
if let Some(window) = focused_workspace.maximized_window() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
// (alex-ds13): @LGUG2Z Why was this being done below on the monocle?
|
||||
// Should it really be done?
|
||||
//
|
||||
// WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
|
||||
// window.hwnd,
|
||||
// )?)?;
|
||||
|
||||
cross_monitor_monocle_or_max = true;
|
||||
} else if let Some(monocle) = focused_workspace.monocle_container() {
|
||||
if let Some(window) = monocle.focused_window() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
WindowsApi::center_cursor_in_rect(&WindowsApi::window_rect(
|
||||
window.hwnd,
|
||||
)?)?;
|
||||
|
||||
cross_monitor_monocle = true;
|
||||
cross_monitor_monocle_or_max = true;
|
||||
}
|
||||
} else {
|
||||
match direction {
|
||||
@@ -1394,7 +1726,7 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
if !cross_monitor_monocle {
|
||||
if !cross_monitor_monocle_or_max {
|
||||
if let Ok(focused_window) = self.focused_window_mut() {
|
||||
focused_window.focus(self.mouse_follows_focus)?;
|
||||
}
|
||||
@@ -1473,15 +1805,13 @@ impl WindowManager {
|
||||
.ok_or_else(|| anyhow!("there is no container or monitor in this direction"))?;
|
||||
|
||||
{
|
||||
// remove the container from the origin monitor workspace
|
||||
let origin_container = self
|
||||
.focused_workspace_mut()?
|
||||
.remove_container_by_idx(origin_container_idx)
|
||||
.ok_or_else(|| {
|
||||
anyhow!("could not remove container at given origin index")
|
||||
})?;
|
||||
|
||||
self.focused_workspace_mut()?.focus_previous_container();
|
||||
// actually move the container to target monitor using the direction
|
||||
self.move_container_to_monitor(
|
||||
target_monitor_idx,
|
||||
None,
|
||||
true,
|
||||
Some(direction),
|
||||
)?;
|
||||
|
||||
// focus the target monitor
|
||||
self.focus_monitor(target_monitor_idx)?;
|
||||
@@ -1501,79 +1831,6 @@ impl WindowManager {
|
||||
// get a mutable ref to the focused workspace on the target monitor
|
||||
let target_workspace = self.focused_workspace_mut()?;
|
||||
|
||||
match direction {
|
||||
OperationDirection::Left => {
|
||||
// insert the origin container into the focused workspace on the target monitor
|
||||
// at the back (or rightmost position) if we are moving across a boundary to
|
||||
// the left (back = right side of the target)
|
||||
match target_workspace.layout() {
|
||||
Layout::Default(layout) => match layout {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
target_workspace.add_container_to_front(origin_container);
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace
|
||||
.insert_container_at_idx(0, origin_container);
|
||||
} else {
|
||||
target_workspace
|
||||
.add_container_to_back(origin_container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.add_container_to_back(origin_container);
|
||||
}
|
||||
},
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_back(origin_container);
|
||||
}
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
// insert the origin container into the focused workspace on the target monitor
|
||||
// at the front (or leftmost position) if we are moving across a boundary to the
|
||||
// right (front = left side of the target)
|
||||
match target_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(target_workspace.containers().len());
|
||||
|
||||
match layout {
|
||||
DefaultLayout::RightMainVerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace
|
||||
.add_container_to_back(origin_container);
|
||||
} else {
|
||||
target_workspace.insert_container_at_idx(
|
||||
target_index,
|
||||
origin_container,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.insert_container_at_idx(
|
||||
target_index,
|
||||
origin_container,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_front(origin_container);
|
||||
}
|
||||
}
|
||||
}
|
||||
OperationDirection::Up | OperationDirection::Down => {
|
||||
// insert the origin container into the focused workspace on the target monitor
|
||||
// at the position where the currently focused container on that workspace is
|
||||
target_workspace.insert_container_at_idx(
|
||||
target_workspace.focused_container_idx(),
|
||||
origin_container,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// if there is only one container on the target workspace after the insertion
|
||||
// it means that there won't be one swapped back, so we have to decrement the
|
||||
// focused position
|
||||
@@ -1718,7 +1975,12 @@ impl WindowManager {
|
||||
|
||||
tracing::info!("cycling container windows");
|
||||
|
||||
let container = self.focused_container_mut()?;
|
||||
let container =
|
||||
if let Some(container) = self.focused_workspace_mut()?.monocle_container_mut() {
|
||||
container
|
||||
} else {
|
||||
self.focused_container_mut()?
|
||||
};
|
||||
|
||||
let len = NonZeroUsize::new(container.windows().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
|
||||
@@ -1736,13 +1998,51 @@ impl WindowManager {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn cycle_container_window_index_in_direction(
|
||||
&mut self,
|
||||
direction: CycleDirection,
|
||||
) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
tracing::info!("cycling container window index");
|
||||
|
||||
let container =
|
||||
if let Some(container) = self.focused_workspace_mut()?.monocle_container_mut() {
|
||||
container
|
||||
} else {
|
||||
self.focused_container_mut()?
|
||||
};
|
||||
|
||||
let len = NonZeroUsize::new(container.windows().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
|
||||
|
||||
if len.get() == 1 {
|
||||
bail!("there is only one window in this container");
|
||||
}
|
||||
|
||||
let current_idx = container.focused_window_idx();
|
||||
let next_idx = direction.next_idx(current_idx, len);
|
||||
container.windows_mut().swap(current_idx, next_idx);
|
||||
|
||||
container.focus_window(next_idx);
|
||||
container.load_focused_window();
|
||||
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn focus_container_window(&mut self, idx: usize) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
tracing::info!("focusing container window at index {idx}");
|
||||
|
||||
let container = self.focused_container_mut()?;
|
||||
let container =
|
||||
if let Some(container) = self.focused_workspace_mut()?.monocle_container_mut() {
|
||||
container
|
||||
} else {
|
||||
self.focused_container_mut()?
|
||||
};
|
||||
|
||||
let len = NonZeroUsize::new(container.windows().len())
|
||||
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;
|
||||
@@ -1858,8 +2158,16 @@ impl WindowManager {
|
||||
new_idx
|
||||
};
|
||||
|
||||
let mut target_container_is_stack = false;
|
||||
|
||||
if let Some(container) = workspace.containers().get(adjusted_new_index) {
|
||||
if container.windows().len() > 1 {
|
||||
target_container_is_stack = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(current) = workspace.focused_container() {
|
||||
if current.windows().len() > 1 {
|
||||
if current.windows().len() > 1 && !target_container_is_stack {
|
||||
workspace.focus_container(adjusted_new_index);
|
||||
workspace.move_window_to_container(current_container_idx)?;
|
||||
} else {
|
||||
|
||||
@@ -77,6 +77,7 @@ use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetDesktopWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetLayeredWindowAttributes;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetTopWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||
@@ -105,7 +106,6 @@ use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GW_HWNDNEXT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_ALPHA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SPIF_SENDCHANGE;
|
||||
@@ -128,7 +128,6 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CLOSE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDENUMPROC;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
|
||||
@@ -153,6 +152,7 @@ macro_rules! as_ptr {
|
||||
};
|
||||
}
|
||||
|
||||
use crate::border_manager::Border;
|
||||
pub(crate) use as_ptr;
|
||||
|
||||
pub enum WindowsResult<T, E> {
|
||||
@@ -300,14 +300,21 @@ impl WindowsApi {
|
||||
}
|
||||
}
|
||||
|
||||
if monitors.elements().is_empty() {
|
||||
monitors.elements_mut().push_back(m);
|
||||
} else if let Some(preference) = index_preference {
|
||||
while *preference > monitors.elements().len() {
|
||||
if let Some(preference) = index_preference {
|
||||
while *preference >= monitors.elements().len() {
|
||||
monitors.elements_mut().push_back(Monitor::placeholder());
|
||||
}
|
||||
|
||||
monitors.elements_mut().insert(*preference, m);
|
||||
let current_name = monitors
|
||||
.elements_mut()
|
||||
.get(*preference)
|
||||
.map_or("", |m| m.name());
|
||||
if current_name == "PLACEHOLDER" {
|
||||
let _ = monitors.elements_mut().remove(*preference);
|
||||
monitors.elements_mut().insert(*preference, m);
|
||||
} else {
|
||||
monitors.elements_mut().insert(*preference, m);
|
||||
}
|
||||
} else {
|
||||
monitors.elements_mut().push_back(m);
|
||||
}
|
||||
@@ -445,7 +452,13 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn set_border_pos(hwnd: isize, layout: &Rect, position: isize) -> Result<()> {
|
||||
let flags = { SetWindowPosition::SHOW_WINDOW | SetWindowPosition::NO_ACTIVATE };
|
||||
let flags = {
|
||||
SetWindowPosition::NO_SEND_CHANGING
|
||||
| SetWindowPosition::NO_ACTIVATE
|
||||
| SetWindowPosition::NO_REDRAW
|
||||
| SetWindowPosition::SHOW_WINDOW
|
||||
};
|
||||
|
||||
Self::set_window_pos(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
layout,
|
||||
@@ -1082,10 +1095,14 @@ impl WindowsApi {
|
||||
.process()
|
||||
}
|
||||
|
||||
pub fn create_border_window(name: PCWSTR, instance: isize) -> Result<isize> {
|
||||
pub fn create_border_window(
|
||||
name: PCWSTR,
|
||||
instance: isize,
|
||||
border: *const Border,
|
||||
) -> Result<isize> {
|
||||
unsafe {
|
||||
let hwnd = CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
|
||||
CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOACTIVATE,
|
||||
name,
|
||||
name,
|
||||
WS_POPUP | WS_SYSMENU,
|
||||
@@ -1096,12 +1113,8 @@ impl WindowsApi {
|
||||
None,
|
||||
None,
|
||||
HINSTANCE(as_ptr!(instance)),
|
||||
None,
|
||||
)?;
|
||||
|
||||
SetLayeredWindowAttributes(hwnd, COLORREF(0), 0, LWA_COLORKEY)?;
|
||||
|
||||
hwnd
|
||||
Some(border as _),
|
||||
)?
|
||||
}
|
||||
.process()
|
||||
}
|
||||
@@ -1120,6 +1133,21 @@ impl WindowsApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_transparent(hwnd: isize) -> Result<u8> {
|
||||
unsafe {
|
||||
let mut alpha: u8 = u8::default();
|
||||
let mut color_ref = COLORREF(-1i32 as u32);
|
||||
let mut flags = LWA_ALPHA;
|
||||
GetLayeredWindowAttributes(
|
||||
HWND(as_ptr!(hwnd)),
|
||||
Some(std::ptr::addr_of_mut!(color_ref)),
|
||||
Some(std::ptr::addr_of_mut!(alpha)),
|
||||
Some(std::ptr::addr_of_mut!(flags)),
|
||||
)?;
|
||||
Ok(alpha)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_hidden_window(name: PCWSTR, instance: isize) -> Result<isize> {
|
||||
unsafe {
|
||||
CreateWindowExW(
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
|
||||
|
||||
use crate::border_manager;
|
||||
use crate::container::Container;
|
||||
use crate::window::RuleDebug;
|
||||
use crate::window::Window;
|
||||
@@ -12,6 +8,19 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::winevent_listener;
|
||||
use windows::Win32::Foundation::BOOL;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SendNotifyMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWL_EXSTYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWL_STYLE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::OBJID_WINDOW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_CHILD;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
|
||||
pub extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let containers = unsafe { &mut *(lparam.0 as *mut VecDeque<Container>) };
|
||||
@@ -60,6 +69,15 @@ pub extern "system" fn alt_tab_windows(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
true.into()
|
||||
}
|
||||
|
||||
fn has_filtered_style(hwnd: HWND) -> bool {
|
||||
let style = unsafe { GetWindowLongW(hwnd, GWL_STYLE) as u32 };
|
||||
let ex_style = unsafe { GetWindowLongW(hwnd, GWL_EXSTYLE) as u32 };
|
||||
|
||||
style & WS_CHILD.0 != 0
|
||||
|| ex_style & WS_EX_TOOLWINDOW.0 != 0
|
||||
|| ex_style & WS_EX_NOACTIVATE.0 != 0
|
||||
}
|
||||
|
||||
pub extern "system" fn win_event_hook(
|
||||
_h_win_event_hook: HWINEVENTHOOK,
|
||||
event: u32,
|
||||
@@ -69,8 +87,7 @@ pub extern "system" fn win_event_hook(
|
||||
_id_event_thread: u32,
|
||||
_dwms_event_time: u32,
|
||||
) {
|
||||
// OBJID_WINDOW
|
||||
if id_object != 0 {
|
||||
if id_object != OBJID_WINDOW.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,6 +98,23 @@ pub extern "system" fn win_event_hook(
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// this forwards the message to the window's border when it moves or is destroyed
|
||||
// see border_manager/border.rs
|
||||
if matches!(
|
||||
winevent,
|
||||
WinEvent::ObjectLocationChange | WinEvent::ObjectDestroy
|
||||
) && !has_filtered_style(hwnd)
|
||||
{
|
||||
let border_window = border_manager::window_border(hwnd.0 as isize);
|
||||
|
||||
if let Some(border) = border_window {
|
||||
unsafe {
|
||||
let _ =
|
||||
SendNotifyMessageW(border.hwnd(), event, WPARAM(0), LPARAM(hwnd.0 as isize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let event_type = match WindowManagerEvent::from_win_event(winevent, window) {
|
||||
None => {
|
||||
tracing::trace!(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user