mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-19 14:26:58 +01:00
Compare commits
86 Commits
feature/ba
...
feature/mf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
559fa55b9f | ||
|
|
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
|
||||
2028
Cargo.lock
generated
2028
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,15 +16,15 @@ 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 }
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
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::CentralPanel;
|
||||
@@ -18,11 +28,10 @@ use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
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,6 +47,7 @@ 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 right_widgets: Vec<Box<dyn BarWidget>>,
|
||||
@@ -146,16 +156,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 +248,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 +286,7 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +294,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,8 +328,8 @@ 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::Right => right_widgets[idx] = boxed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +342,7 @@ impl Komobar {
|
||||
|
||||
self.komorebi_notification_state = komorebi_notification_state;
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
rx_gui: Receiver<komorebi_client::Notification>,
|
||||
@@ -285,6 +351,7 @@ 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![],
|
||||
right_widgets: vec![],
|
||||
@@ -294,6 +361,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 +422,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,18 +467,35 @@ 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);
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
@@ -420,7 +503,7 @@ impl eframe::App for Komobar {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Side {
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
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,55 @@ 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>,
|
||||
/// 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 +143,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 +173,15 @@ 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,
|
||||
}
|
||||
|
||||
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,10 +1,12 @@
|
||||
use crate::bar::apply_theme;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::render::RenderConfig;
|
||||
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 crossbeam_channel::TryRecvError;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ColorImage;
|
||||
@@ -83,14 +85,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();
|
||||
@@ -110,6 +104,7 @@ impl From<&KomorebiConfig> for Komorebi {
|
||||
work_area_offset: None,
|
||||
focused_container_information: (vec![], vec![], 0),
|
||||
stack_accent: None,
|
||||
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
|
||||
})),
|
||||
workspaces: value.workspaces,
|
||||
layout: value.layout,
|
||||
@@ -129,97 +124,126 @@ 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()
|
||||
// NOTE: There should always be at least one workspace if the bar is connected to komorebi.
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for (i, (ws, should_show)) in
|
||||
komorebi_notification_state.workspaces.iter().enumerate()
|
||||
{
|
||||
update = Some(ws.to_string());
|
||||
let mut proceed = true;
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
|
||||
.is_err()
|
||||
if *should_show
|
||||
&& ui
|
||||
.add(SelectableLabel::new(
|
||||
komorebi_notification_state.selected_workspace.eq(ws),
|
||||
ws.to_string(),
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
|
||||
proceed = false;
|
||||
}
|
||||
update = Some(ws.to_string());
|
||||
let mut proceed = true;
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i))
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: FocusWorkspaceNumber");
|
||||
proceed = false;
|
||||
}
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
|
||||
proceed = false;
|
||||
}
|
||||
if proceed
|
||||
&& komorebi_client::send_message(
|
||||
&SocketMessage::FocusMonitorWorkspaceNumber(
|
||||
komorebi_notification_state.monitor_index,
|
||||
i,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusWorkspaceNumber"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed && komorebi_client::send_message(&SocketMessage::Retile).is_err() {
|
||||
tracing::error!("could not send message to komorebi: Retile");
|
||||
if proceed
|
||||
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if proceed
|
||||
&& komorebi_client::send_message(
|
||||
&SocketMessage::RetileWithResizeDimensions,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: Retile");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(update) = update {
|
||||
komorebi_notification_state.selected_workspace = update;
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if let Some(layout) = self.layout {
|
||||
if layout.enable {
|
||||
if ui
|
||||
.add(
|
||||
Label::new(komorebi_notification_state.layout.to_string())
|
||||
.selectable(false)
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
match komorebi_notification_state.layout {
|
||||
KomorebiLayout::Default(_) => {
|
||||
if komorebi_client::send_message(&SocketMessage::CycleLayout(
|
||||
CycleDirection::Next,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: CycleLayout");
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
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::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::Paused => {
|
||||
if komorebi_client::send_message(&SocketMessage::TogglePause)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: TogglePause"
|
||||
);
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Custom => {}
|
||||
}
|
||||
KomorebiLayout::Custom => {}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,170 +251,174 @@ impl BarWidget for Komorebi {
|
||||
if configuration_switcher.enable {
|
||||
for (name, location) in configuration_switcher.configurations.iter() {
|
||||
let path = PathBuf::from(location);
|
||||
if path.is_file()
|
||||
&& ui
|
||||
if path.is_file() {
|
||||
config.apply_on_widget(true, ui,|ui|{
|
||||
if 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;
|
||||
}
|
||||
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
|
||||
let mut proceed = true;
|
||||
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
|
||||
canonicalized,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: ReplaceConfiguration"
|
||||
);
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
if let Some(rect) = komorebi_notification_state.work_area_offset {
|
||||
if proceed {
|
||||
match komorebi_client::send_query(&SocketMessage::Query(
|
||||
komorebi_client::StateQuery::FocusedMonitorIndex,
|
||||
)) {
|
||||
Ok(idx) => {
|
||||
if let Ok(monitor_idx) = idx.parse::<usize>() {
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
if let Some(rect) = komorebi_notification_state.work_area_offset {
|
||||
if proceed {
|
||||
match komorebi_client::send_query(&SocketMessage::Query(
|
||||
komorebi_client::StateQuery::FocusedMonitorIndex,
|
||||
)) {
|
||||
Ok(idx) => {
|
||||
if let Ok(monitor_idx) = idx.parse::<usize>() {
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::MonitorWorkAreaOffset(
|
||||
monitor_idx,
|
||||
rect,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MonitorWorkAreaOffset"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: Query"
|
||||
);
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: Query"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(focused_window) = self.focused_window {
|
||||
if focused_window.enable {
|
||||
let titles = &komorebi_notification_state.focused_container_information.0;
|
||||
let icons = &komorebi_notification_state.focused_container_information.1;
|
||||
let focused_window_idx =
|
||||
komorebi_notification_state.focused_container_information.2;
|
||||
if !titles.is_empty() {
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
let icons = &komorebi_notification_state.focused_container_information.1;
|
||||
let focused_window_idx =
|
||||
komorebi_notification_state.focused_container_information.2;
|
||||
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
let iter = titles.iter().zip(icons.iter());
|
||||
|
||||
for (i, (title, icon)) in iter.enumerate() {
|
||||
if focused_window.show_icon {
|
||||
if let Some(img) = icon {
|
||||
ui.add(
|
||||
Image::from(&img_to_texture(ctx, img))
|
||||
.maintain_aspect_ratio(true)
|
||||
.max_height(15.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if i == focused_window_idx {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
let layout_job = LayoutJob::simple(
|
||||
title.to_string(),
|
||||
font_id.clone(),
|
||||
komorebi_notification_state
|
||||
.stack_accent
|
||||
.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
if titles.len() > 1 {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job).selectable(false).truncate(),
|
||||
);
|
||||
} else {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title).selectable(false).truncate(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
if custom_ui
|
||||
.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title)
|
||||
.selectable(false)
|
||||
.sense(Sense::click())
|
||||
.truncate(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
false,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
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 komorebi_client::send_message(&SocketMessage::FocusStackWindow(i))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusStackWindow"
|
||||
);
|
||||
}
|
||||
if i == focused_window_idx {
|
||||
let font_id = ctx
|
||||
.style()
|
||||
.text_styles
|
||||
.get(&TextStyle::Body)
|
||||
.cloned()
|
||||
.unwrap_or_else(FontId::default);
|
||||
|
||||
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
let layout_job = LayoutJob::simple(
|
||||
title.to_string(),
|
||||
font_id.clone(),
|
||||
komorebi_notification_state
|
||||
.stack_accent
|
||||
.unwrap_or(ctx.style().visuals.selection.stroke.color),
|
||||
100.0,
|
||||
);
|
||||
|
||||
if titles.len() > 1 {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job).selectable(false).truncate(),
|
||||
);
|
||||
} else {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
custom_ui.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title).selectable(false).truncate(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
|
||||
if custom_ui
|
||||
.add_sized_left_to_right(
|
||||
Vec2::new(
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(title)
|
||||
.selectable(false)
|
||||
.sense(Sense::click())
|
||||
.truncate(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::MouseFollowsFocus(false),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::FocusStackWindow(i),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: FocusStackWindow"
|
||||
);
|
||||
}
|
||||
|
||||
if komorebi_client::send_message(
|
||||
&SocketMessage::MouseFollowsFocus(
|
||||
komorebi_notification_state.mouse_follows_focus,
|
||||
),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!(
|
||||
"could not send message to komorebi: MouseFollowsFocus"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,7 +432,7 @@ fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KomorebiNotificationState {
|
||||
pub workspaces: Vec<String>,
|
||||
pub workspaces: Vec<(String, bool)>,
|
||||
pub selected_workspace: String,
|
||||
pub focused_container_information: (Vec<String>, Vec<Option<RgbaImage>>, usize),
|
||||
pub layout: KomorebiLayout,
|
||||
@@ -412,6 +440,7 @@ pub struct KomorebiNotificationState {
|
||||
pub mouse_follows_focus: bool,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub stack_accent: Option<Color32>,
|
||||
pub monitor_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -445,93 +474,115 @@ 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.mouse_follows_focus = notification.state.mouse_follows_focus;
|
||||
self.monitor_index = monitor_index;
|
||||
|
||||
let monitor = ¬ification.state.monitors.elements()[monitor_index];
|
||||
self.work_area_offset =
|
||||
notification.state.monitors.elements()[monitor_index].work_area_offset();
|
||||
self.mouse_follows_focus = notification.state.mouse_follows_focus;
|
||||
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
let monitor = ¬ification.state.monitors.elements()[monitor_index];
|
||||
self.work_area_offset =
|
||||
notification.state.monitors.elements()[monitor_index].work_area_offset();
|
||||
|
||||
let mut workspaces = vec![];
|
||||
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
|
||||
.name()
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
|
||||
for (i, ws) in monitor.workspaces().iter().enumerate() {
|
||||
let should_add = if self.hide_empty_workspaces {
|
||||
focused_workspace_idx == i || !ws.containers().is_empty()
|
||||
} else {
|
||||
true
|
||||
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
|
||||
};
|
||||
|
||||
workspaces.push((
|
||||
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
|
||||
should_show,
|
||||
));
|
||||
}
|
||||
|
||||
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 should_add {
|
||||
workspaces.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)));
|
||||
if !*monitor.workspaces()[focused_workspace_idx].tile() {
|
||||
self.layout = KomorebiLayout::Floating;
|
||||
}
|
||||
}
|
||||
|
||||
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 notification.state.is_paused {
|
||||
self.layout = KomorebiLayout::Paused;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
mod bar;
|
||||
mod battery;
|
||||
mod config;
|
||||
mod cpu;
|
||||
mod date;
|
||||
mod komorebi;
|
||||
mod media;
|
||||
mod memory;
|
||||
mod network;
|
||||
mod render;
|
||||
mod storage;
|
||||
mod time;
|
||||
mod ui;
|
||||
@@ -14,29 +16,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 +66,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 +184,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 +198,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 +211,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(config.transparency_alpha.is_some())
|
||||
.with_taskbar(false);
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: viewport_builder,
|
||||
..Default::default()
|
||||
@@ -291,7 +332,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 +376,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,6 +31,8 @@ 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 {
|
||||
@@ -42,6 +45,8 @@ impl From<NetworkConfig> for Network {
|
||||
|
||||
let mut default_interface = String::new();
|
||||
|
||||
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = interface.friendly_name {
|
||||
default_interface.clone_from(&friendly_name);
|
||||
@@ -50,13 +55,32 @@ impl From<NetworkConfig> for Network {
|
||||
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),
|
||||
))
|
||||
last_state_data.push(match prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{} | {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {} | {} {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"\u{2211}DOWN: {} | \u{2211}UP: {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::IconAndText => format!(
|
||||
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,14 +89,40 @@ impl From<NetworkConfig> for Network {
|
||||
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(),
|
||||
))
|
||||
last_state_transmitted.push(match prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{: >width$}/s | {: >width$}/s",
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {: >width$}/s | {} {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"DOWN: {: >width$}/s | UP: {: >width$}/s",
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
LabelPrefix::IconAndText => format!(
|
||||
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.transmitted(), 1),
|
||||
width =
|
||||
value.network_activity_fill_characters.unwrap_or_default(),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +135,7 @@ impl From<NetworkConfig> for Network {
|
||||
networks_network_activity,
|
||||
default_interface,
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: prefix,
|
||||
show_total_data_transmitted: value.show_total_data_transmitted,
|
||||
show_network_activity: value.show_network_activity,
|
||||
network_activity_fill_characters: value
|
||||
@@ -105,6 +156,7 @@ pub struct Network {
|
||||
networks_total_data_transmitted: Networks,
|
||||
networks_network_activity: Networks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
default_interface: String,
|
||||
last_state_total_data_transmitted: Vec<String>,
|
||||
last_state_network_activity: Vec<String>,
|
||||
@@ -138,14 +190,62 @@ impl Network {
|
||||
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,
|
||||
))
|
||||
outputs.push(match self.label_prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{: >width$}/s | {: >width$}/s",
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {: >width$}/s | {} {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"DOWN: {: >width$}/s | UP: {: >width$}/s",
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
),
|
||||
LabelPrefix::IconAndText => {
|
||||
format!(
|
||||
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval
|
||||
),
|
||||
width = self.network_activity_fill_characters,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,13 +276,32 @@ impl Network {
|
||||
|
||||
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),
|
||||
))
|
||||
outputs.push(match self.label_prefix {
|
||||
LabelPrefix::None => format!(
|
||||
"{} | {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Icon => format!(
|
||||
"{} {} | {} {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::Text => format!(
|
||||
"\u{2211}DOWN: {} | \u{2211}UP: {}",
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
LabelPrefix::IconAndText => format!(
|
||||
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
|
||||
egui_phosphor::regular::ARROW_FAT_DOWN,
|
||||
to_pretty_bytes(data.total_received(), 1),
|
||||
egui_phosphor::regular::ARROW_FAT_UP,
|
||||
to_pretty_bytes(data.total_transmitted(), 1),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,21 +317,21 @@ impl Network {
|
||||
}
|
||||
|
||||
impl BarWidget for Network {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.show_total_data_transmitted {
|
||||
for output in self.total_data_transmitted() {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if self.show_network_activity {
|
||||
for output in self.network_activity() {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(Label::new(output).selectable(false));
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(WIDGET_SPACING);
|
||||
}
|
||||
|
||||
if self.enable {
|
||||
@@ -227,33 +346,42 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
265
komorebi-bar/src/render.rs
Normal file
265
komorebi-bar/src/render.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
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;
|
||||
|
||||
#[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 {
|
||||
/// 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 {
|
||||
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 new() -> Self {
|
||||
Self {
|
||||
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::Right => 0.0,
|
||||
},
|
||||
None => 0.0,
|
||||
},
|
||||
right: match self.alignment {
|
||||
Some(align) => match align {
|
||||
Alignment::Left => 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
|
||||
}
|
||||
}
|
||||
@@ -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,7 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||
pub use komorebi::colour::Colour;
|
||||
pub use komorebi::colour::Rgb;
|
||||
pub use komorebi::config_generation::ApplicationConfiguration;
|
||||
@@ -44,6 +45,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;
|
||||
@@ -96,3 +98,29 @@ pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
pub fn subscribe_with_options(
|
||||
name: &str,
|
||||
options: SubscribeOptions,
|
||||
) -> std::io::Result<UnixListener> {
|
||||
let socket = DATA_DIR.join(name);
|
||||
|
||||
match std::fs::remove_file(&socket) {
|
||||
Ok(()) => {}
|
||||
Err(error) => match error.kind() {
|
||||
std::io::ErrorKind::NotFound => {}
|
||||
_ => {
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let listener = UnixListener::bind(&socket)?;
|
||||
|
||||
send_message(&SocketMessage::AddSubscriberSocketWithOptions(
|
||||
name.to_string(),
|
||||
options,
|
||||
))?;
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
@@ -1,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
|
||||
|
||||
@@ -3,47 +3,78 @@ 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 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 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::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::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::BeginPaint;
|
||||
use windows::Win32::Graphics::Gdi::CreatePen;
|
||||
use windows::Win32::Graphics::Gdi::DeleteObject;
|
||||
use windows::Win32::Graphics::Gdi::CreateRectRgn;
|
||||
use windows::Win32::Graphics::Gdi::EndPaint;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::Rectangle;
|
||||
use windows::Win32::Graphics::Gdi::RoundRect;
|
||||
use windows::Win32::Graphics::Gdi::SelectObject;
|
||||
use windows::Win32::Graphics::Gdi::PAINTSTRUCT;
|
||||
use windows::Win32::Graphics::Gdi::PS_INSIDEFRAME;
|
||||
use windows::Win32::Graphics::Gdi::PS_SOLID;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
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::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_SIZE;
|
||||
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>) };
|
||||
@@ -83,7 +114,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()
|
||||
@@ -110,32 +140,80 @@ impl Border {
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
hwnd: hwnd_receiver.recv()?,
|
||||
})
|
||||
let hwnd = hwnd_receiver.recv()?;
|
||||
let border = Self { hwnd };
|
||||
|
||||
// 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!(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 {
|
||||
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
||||
let mut render_targets = RENDER_TARGETS.lock();
|
||||
render_targets.insert(hwnd, render_target);
|
||||
Ok(border)
|
||||
},
|
||||
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<()> {
|
||||
pub fn update(&self, rect: &Rect, should_invalidate: bool) -> color_eyre::Result<()> {
|
||||
// Make adjustments to the border
|
||||
let mut rect = *rect;
|
||||
rect.add_margin(BORDER_WIDTH.load(Ordering::SeqCst));
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::SeqCst));
|
||||
rect.add_margin(BORDER_WIDTH.load(Ordering::Relaxed));
|
||||
rect.add_padding(-BORDER_OFFSET.load(Ordering::Relaxed));
|
||||
|
||||
// Update the position of the border if required
|
||||
// This effectively handles WM_MOVE
|
||||
// Also if I remove this no borders render at all lol
|
||||
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.
|
||||
@@ -158,72 +236,104 @@ impl Border {
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_PAINT => {
|
||||
let mut ps = PAINTSTRUCT::default();
|
||||
let hdc = BeginPaint(window, &mut ps);
|
||||
|
||||
// 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)
|
||||
WM_SIZE | WM_PAINT => {
|
||||
if let Ok(rect) = WindowsApi::window_rect(window.0 as isize) {
|
||||
let render_targets = RENDER_TARGETS.lock();
|
||||
if let Some(render_target) = render_targets.get(&(window.0 as isize)) {
|
||||
let pixel_size = D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
};
|
||||
|
||||
// Set up the brush to draw the border
|
||||
let hpen = CreatePen(
|
||||
PS_SOLID | PS_INSIDEFRAME,
|
||||
BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
COLORREF(window_kind_colour(window_kind)),
|
||||
);
|
||||
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
|
||||
let hbrush = WindowsApi::create_solid_brush(0);
|
||||
let 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,
|
||||
};
|
||||
|
||||
// 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);
|
||||
let _ = render_target.Resize(&pixel_size);
|
||||
|
||||
// Get window kind and color
|
||||
let window_kind = FOCUS_STATE
|
||||
.lock()
|
||||
.get(&(window.0 as isize))
|
||||
.copied()
|
||||
.unwrap_or(WindowKind::Unfocused);
|
||||
|
||||
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(BRUSH_PROPERTIES.deref()))
|
||||
{
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match STYLE.load() {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
let radius = 8.0 + border_width as f32 / 2.0;
|
||||
let rounded_rect = D2D1_ROUNDED_RECT {
|
||||
rect,
|
||||
radiusX: radius,
|
||||
radiusY: radius,
|
||||
};
|
||||
|
||||
render_target.DrawRoundedRectangle(
|
||||
&rounded_rect,
|
||||
&brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
let rect = D2D_RECT_F {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
};
|
||||
|
||||
render_target.DrawRectangle(
|
||||
&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);
|
||||
|
||||
// If we don't do this we'll get spammed with WM_PAINT according to Raymond Chen
|
||||
// https://stackoverflow.com/questions/41783234/why-does-my-call-to-d2d1rendertargetdrawtext-result-in-a-wm-paint-being-se#comment70756781_41783234
|
||||
let _ = BeginPaint(window, &mut PAINTSTRUCT::default());
|
||||
let _ = EndPaint(window, &PAINTSTRUCT::default());
|
||||
}
|
||||
// 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_DESTROY => {
|
||||
|
||||
@@ -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;
|
||||
@@ -30,6 +29,10 @@ use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
use windows::Win32::System::Threading::GetCurrentThread;
|
||||
use windows::Win32::System::Threading::SetThreadPriority;
|
||||
use windows::Win32::System::Threading::THREAD_PRIORITY_TIME_CRITICAL;
|
||||
|
||||
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
|
||||
pub static BORDER_OFFSET: AtomicI32 = AtomicI32::new(-1);
|
||||
@@ -49,15 +52,19 @@ 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 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 +80,8 @@ fn event_rx() -> Receiver<Notification> {
|
||||
channel().1.clone()
|
||||
}
|
||||
|
||||
pub fn send_notification() {
|
||||
if event_tx().try_send(Notification).is_err() {
|
||||
pub fn send_notification(hwnd: Option<isize>) {
|
||||
if event_tx().try_send(Notification(hwnd)).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,17 +126,27 @@ fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
||||
WindowKind::Single => FOCUSED.load(Ordering::SeqCst),
|
||||
WindowKind::Stack => STACK.load(Ordering::SeqCst),
|
||||
WindowKind::Monocle => MONOCLE.load(Ordering::SeqCst),
|
||||
WindowKind::Floating => FLOATING.load(Ordering::SeqCst),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
std::thread::spawn(move || {
|
||||
unsafe {
|
||||
if let Err(error) = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)
|
||||
{
|
||||
tracing::error!("{error}");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
|
||||
loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -139,19 +157,30 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
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 weak_pending_move_op = Arc::downgrade(&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 +249,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;
|
||||
@@ -345,16 +389,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,8 +414,14 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
for (idx, c) in ws.containers().iter().enumerate() {
|
||||
let hwnd = c.focused_window().copied().unwrap_or_default().hwnd;
|
||||
let notification_hwnd = notification.0.unwrap_or_default();
|
||||
|
||||
// Update border when moving or resizing with mouse
|
||||
if pending_move_op.is_some() && idx == ws.focused_container_idx() {
|
||||
if pending_move_op.is_some()
|
||||
&& idx == ws.focused_container_idx()
|
||||
&& hwnd == notification_hwnd
|
||||
{
|
||||
let restore_z_order = Z_ORDER.load();
|
||||
Z_ORDER.store(ZOrder::TopMost);
|
||||
|
||||
@@ -375,7 +429,19 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
c.focused_window().copied().unwrap_or_default().hwnd,
|
||||
)?;
|
||||
|
||||
while WindowsApi::lbutton_is_pressed() {
|
||||
// We create a new variable to track the actual pending move op so
|
||||
// that the other variable `pending_move_op` still holds the
|
||||
// pending move info so that when the move ends we know on the next
|
||||
// notification that the previous pending move and pending move are
|
||||
// different (because a move just finished) and still handle the
|
||||
// notification. If otherwise we updated the pending_move_op here
|
||||
// directly then the next pending move after finish would be the
|
||||
// same because we had already updated it here.
|
||||
let mut sync_pending_move_op =
|
||||
weak_pending_move_op.upgrade().and_then(|p| *p);
|
||||
while sync_pending_move_op.is_some() {
|
||||
sync_pending_move_op =
|
||||
weak_pending_move_op.upgrade().and_then(|p| *p);
|
||||
let border = match borders.entry(c.id().clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
@@ -393,7 +459,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
if rect != new_rect {
|
||||
rect = new_rect;
|
||||
border.update(&rect, true)?;
|
||||
border.update(&rect, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +512,106 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
|
||||
{
|
||||
let restore_z_order = Z_ORDER.load();
|
||||
Z_ORDER.store(ZOrder::TopMost);
|
||||
|
||||
'windows: for window in ws.floating_windows() {
|
||||
let hwnd = window.hwnd;
|
||||
let notification_hwnd = notification.0.unwrap_or_default();
|
||||
|
||||
if pending_move_op.is_some() && hwnd == notification_hwnd {
|
||||
let mut rect = WindowsApi::window_rect(hwnd)?;
|
||||
|
||||
// Check comment above for containers move
|
||||
let mut sync_pending_move_op =
|
||||
weak_pending_move_op.upgrade().and_then(|p| *p);
|
||||
while sync_pending_move_op.is_some() {
|
||||
sync_pending_move_op =
|
||||
weak_pending_move_op.upgrade().and_then(|p| *p);
|
||||
let border = match borders.entry(hwnd.to_string()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) =
|
||||
Border::create(&hwnd.to_string())
|
||||
{
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_rect = WindowsApi::window_rect(hwnd)?;
|
||||
|
||||
if rect != new_rect {
|
||||
rect = new_rect;
|
||||
border.update(&rect, true)?;
|
||||
}
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let border = match borders.entry(window.hwnd.to_string()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(&window.hwnd.to_string())
|
||||
{
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
|
||||
|
||||
let mut should_destroy = false;
|
||||
|
||||
if let Some(notification_hwnd) = notification.0 {
|
||||
if notification_hwnd != window.hwnd {
|
||||
should_destroy = true;
|
||||
}
|
||||
}
|
||||
|
||||
if WindowsApi::foreground_window().unwrap_or_default()
|
||||
!= window.hwnd
|
||||
{
|
||||
should_destroy = true;
|
||||
}
|
||||
|
||||
if should_destroy {
|
||||
border.destroy()?;
|
||||
borders.remove(&window.hwnd.to_string());
|
||||
borders_monitors.remove(&window.hwnd.to_string());
|
||||
continue 'windows;
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
let mut last_focus_state = None;
|
||||
let new_focus_state = WindowKind::Floating;
|
||||
{
|
||||
let mut focus_state = FOCUS_STATE.lock();
|
||||
last_focus_state =
|
||||
focus_state.insert(border.hwnd, new_focus_state);
|
||||
}
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
|
||||
let should_invalidate = match last_focus_state {
|
||||
None => true,
|
||||
Some(last_focus_state) => last_focus_state != new_focus_state,
|
||||
};
|
||||
|
||||
border.update(&rect, should_invalidate)?;
|
||||
}
|
||||
|
||||
Z_ORDER.store(restore_z_order);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,6 +620,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);
|
||||
|
||||
|
||||
@@ -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,7 @@ use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::KomorebiTheme;
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
@@ -27,6 +28,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 +49,7 @@ pub enum SocketMessage {
|
||||
StackWindow(OperationDirection),
|
||||
UnstackWindow,
|
||||
CycleStack(CycleDirection),
|
||||
CycleStackIndex(CycleDirection),
|
||||
FocusStackWindow(usize),
|
||||
StackAll,
|
||||
UnstackAll,
|
||||
@@ -77,6 +80,7 @@ pub enum SocketMessage {
|
||||
ToggleMonocle,
|
||||
ToggleMaximize,
|
||||
ToggleWindowContainerBehaviour,
|
||||
ToggleFloatOverride,
|
||||
WindowHidingBehaviour(HidingBehaviour),
|
||||
ToggleCrossMonitorMoveBehaviour,
|
||||
CrossMonitorMoveBehaviour(MoveBehaviour),
|
||||
@@ -90,6 +94,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 +106,7 @@ pub enum SocketMessage {
|
||||
Stop,
|
||||
TogglePause,
|
||||
Retile,
|
||||
RetileWithResizeDimensions,
|
||||
QuickSave,
|
||||
QuickLoad,
|
||||
Save(PathBuf),
|
||||
@@ -108,6 +115,7 @@ pub enum SocketMessage {
|
||||
CycleFocusWorkspace(CycleDirection),
|
||||
FocusMonitorNumber(usize),
|
||||
FocusLastWorkspace,
|
||||
CloseWorkspace,
|
||||
FocusWorkspaceNumber(usize),
|
||||
FocusWorkspaceNumbers(usize),
|
||||
FocusMonitorWorkspaceNumber(usize, usize),
|
||||
@@ -138,6 +146,7 @@ pub enum SocketMessage {
|
||||
WatchConfiguration(bool),
|
||||
CompleteConfiguration,
|
||||
AltFocusHack(bool),
|
||||
Theme(KomorebiTheme),
|
||||
Animation(bool),
|
||||
AnimationDuration(u64),
|
||||
AnimationFps(u64),
|
||||
@@ -174,7 +183,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 +202,7 @@ pub enum SocketMessage {
|
||||
RemoveTitleBar(ApplicationIdentifier, String),
|
||||
ToggleTitleBars,
|
||||
AddSubscriberSocket(String),
|
||||
AddSubscriberSocketWithOptions(String, SubscribeOptions),
|
||||
RemoveSubscriberSocket(String),
|
||||
AddSubscriberPipe(String),
|
||||
RemoveSubscriberPipe(String),
|
||||
@@ -217,6 +228,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,
|
||||
@@ -294,6 +311,7 @@ pub enum WindowKind {
|
||||
Stack,
|
||||
Monocle,
|
||||
Unfocused,
|
||||
Floating,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@@ -331,7 +349,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 +367,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 +442,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
|
||||
|
||||
@@ -20,6 +20,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;
|
||||
@@ -139,7 +140,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 +159,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 +178,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>> =
|
||||
@@ -224,7 +228,6 @@ lazy_static! {
|
||||
|
||||
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);
|
||||
@@ -297,18 +300,36 @@ 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(_))
|
||||
);
|
||||
|
||||
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 +337,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![];
|
||||
|
||||
@@ -38,6 +38,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 +272,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 || {
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::workspace::Workspace;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Layout;
|
||||
use crate::OperationDirection;
|
||||
use crate::WindowsApi;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -178,66 +179,90 @@ 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 => {
|
||||
target_workspace.floating_windows_mut().push(window);
|
||||
} else {
|
||||
let container = workspace
|
||||
.remove_focused_container()
|
||||
.ok_or_else(|| anyhow!("there is no container"))?;
|
||||
|
||||
let workspaces = self.workspaces_mut();
|
||||
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
|
||||
None => {
|
||||
workspaces.resize(target_workspace_idx + 1, Workspace::default());
|
||||
workspaces.get_mut(target_workspace_idx).unwrap()
|
||||
}
|
||||
Some(workspace) => workspace,
|
||||
};
|
||||
|
||||
match direction {
|
||||
Some(OperationDirection::Left) => match target_workspace.layout() {
|
||||
Layout::Default(layout) => match layout {
|
||||
DefaultLayout::RightMainVerticalStack => {
|
||||
target_workspace.add_container_to_front(container);
|
||||
}
|
||||
DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace.add_container_to_back(container);
|
||||
target_workspace.insert_container_at_idx(0, container);
|
||||
} else {
|
||||
target_workspace.insert_container_at_idx(target_index, container);
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.insert_container_at_idx(target_index, container);
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
},
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
},
|
||||
Some(OperationDirection::Right) => match target_workspace.layout() {
|
||||
Layout::Default(layout) => {
|
||||
let target_index =
|
||||
layout.leftmost_index(target_workspace.containers().len());
|
||||
|
||||
match layout {
|
||||
DefaultLayout::RightMainVerticalStack
|
||||
| DefaultLayout::UltrawideVerticalStack => {
|
||||
if target_workspace.containers().len() == 1 {
|
||||
target_workspace.add_container_to_back(container);
|
||||
} else {
|
||||
target_workspace
|
||||
.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
target_workspace.insert_container_at_idx(target_index, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_front(container);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
Layout::Custom(_) => {
|
||||
target_workspace.add_container_to_front(container);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
target_workspace.add_container_to_back(container);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
if should_update {
|
||||
tracing::info!("updated work area for {}", monitor.device_id());
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"work areas match, reconciliation not required for {}",
|
||||
@@ -219,7 +219,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
);
|
||||
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"resolutions match, reconciliation not required for {}",
|
||||
@@ -406,7 +406,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Second retile to fix DPI/resolution related jank
|
||||
wm.retile_all(true)?;
|
||||
// Border updates to fix DPI/resolution related jank
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,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,6 +62,7 @@ use crate::winevent_listener;
|
||||
use crate::GlobalState;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::ANIMATION_DURATION;
|
||||
use crate::ANIMATION_ENABLED;
|
||||
use crate::ANIMATION_FPS;
|
||||
@@ -68,8 +70,8 @@ use crate::ANIMATION_STYLE;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::FLOAT_IDENTIFIERS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
@@ -79,6 +81,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 +109,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 +199,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 +247,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 +414,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),
|
||||
@@ -619,6 +646,11 @@ impl WindowManager {
|
||||
border_manager::destroy_all_borders()?;
|
||||
self.retile_all(false)?
|
||||
}
|
||||
SocketMessage::RetileWithResizeDimensions => {
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(false, Ordering::SeqCst);
|
||||
border_manager::destroy_all_borders()?;
|
||||
self.retile_all(true)?
|
||||
}
|
||||
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 +774,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
|
||||
@@ -1312,6 +1382,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 +1417,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;
|
||||
@@ -1388,7 +1503,7 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
}
|
||||
SocketMessage::BorderColour(kind, r, g, b) => match kind {
|
||||
@@ -1404,6 +1519,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);
|
||||
@@ -1522,18 +1640,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 +1674,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();
|
||||
@@ -440,8 +507,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;
|
||||
@@ -471,11 +537,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,18 +558,13 @@ 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
|
||||
@@ -518,11 +576,15 @@ impl WindowManager {
|
||||
self.focus_monitor(target_monitor_idx)?;
|
||||
self.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) => {
|
||||
@@ -636,13 +698,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,
|
||||
|
||||
@@ -20,6 +20,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;
|
||||
@@ -35,8 +36,9 @@ 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 +51,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 +71,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 +99,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 +112,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 +136,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 +195,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 +233,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 +250,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 +263,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: Determine focus follows mouse implementation (default: None)
|
||||
#[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)
|
||||
@@ -304,10 +324,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,6 +365,10 @@ 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)]
|
||||
@@ -359,7 +387,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 +395,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 +410,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 +418,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 +434,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 +466,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;
|
||||
|
||||
@@ -498,6 +557,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 +569,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(
|
||||
@@ -541,7 +606,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 +622,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -622,6 +689,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 +720,7 @@ impl StaticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
|
||||
transparency_manager::TRANSPARENCY_ENABLED
|
||||
@@ -657,7 +728,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 +736,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 +819,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 +965,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 +1002,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 +1032,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 +1052,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 +1085,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 +1106,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 +1149,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 +1166,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 +1207,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(())
|
||||
}
|
||||
@@ -41,9 +41,10 @@ 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;
|
||||
@@ -159,6 +160,31 @@ impl Window {
|
||||
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 new_rect = Rect {
|
||||
left: x_diff + window_x,
|
||||
top: y_diff + window_y,
|
||||
right: current_rect.right,
|
||||
bottom: current_rect.bottom,
|
||||
};
|
||||
//TODO: We might need to take into account the differences in DPI for the new_rect, unless
|
||||
//we can use the xy ratios above to the right/bottom (width/height of window) as well?
|
||||
|
||||
self.set_position(&new_rect, true)
|
||||
}
|
||||
|
||||
pub fn center(&mut self, work_area: &Rect) -> Result<()> {
|
||||
let half_width = work_area.right / 2;
|
||||
let half_weight = work_area.bottom / 2;
|
||||
@@ -181,7 +207,7 @@ impl Window {
|
||||
let mut animation = self.animation;
|
||||
|
||||
border_manager::BORDER_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(Some(self.hwnd));
|
||||
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
|
||||
stackbar_manager::send_notification();
|
||||
@@ -203,7 +229,7 @@ impl Window {
|
||||
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED
|
||||
.store(false, Ordering::SeqCst);
|
||||
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(Some(hwnd));
|
||||
stackbar_manager::send_notification();
|
||||
transparency_manager::send_notification();
|
||||
}
|
||||
@@ -280,7 +306,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<()> {
|
||||
@@ -534,9 +563,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 +593,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 +623,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 +777,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))]
|
||||
@@ -420,6 +508,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 +525,7 @@ impl WindowManager {
|
||||
origin_workspace_idx,
|
||||
target_monitor_idx,
|
||||
target_workspace_idx,
|
||||
floating,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -491,6 +581,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 +594,7 @@ impl WindowManager {
|
||||
j,
|
||||
rule.monitor_index,
|
||||
rule.workspace_index,
|
||||
floating,
|
||||
&mut to_move,
|
||||
);
|
||||
}
|
||||
@@ -513,6 +606,7 @@ impl WindowManager {
|
||||
j,
|
||||
rule.monitor_index,
|
||||
rule.workspace_index,
|
||||
floating,
|
||||
&mut to_move,
|
||||
);
|
||||
}
|
||||
@@ -531,17 +625,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 +682,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 +821,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 +1078,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 +1097,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 {
|
||||
@@ -1149,6 +1403,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 +1413,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,18 +1437,36 @@ impl WindowManager {
|
||||
.get_mut(monitor_idx)
|
||||
.ok_or_else(|| anyhow!("there is no monitor"))?;
|
||||
|
||||
target_monitor.add_container(container, workspace_idx)?;
|
||||
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
target_monitor.focus_workspace(workspace_idx)?;
|
||||
}
|
||||
let target_workspace = target_monitor
|
||||
.focused_workspace_mut()
|
||||
.ok_or_else(|| anyhow!("there is no focused workspace on target monitor"))?;
|
||||
|
||||
if let Some(workspace) = target_monitor.focused_workspace() {
|
||||
if !*workspace.tile() {
|
||||
for hwnd in container_hwnds {
|
||||
Window::from(hwnd).center(target_monitor.work_area_size())?;
|
||||
if let Some(window) = floating_window {
|
||||
target_workspace.floating_windows_mut().push(window);
|
||||
Window::from(window.hwnd)
|
||||
.move_to_area(¤t_area, target_monitor.work_area_size())?;
|
||||
} else if let Some(container) = container {
|
||||
let container_hwnds = container
|
||||
.windows()
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
target_monitor.add_container(container, workspace_idx)?;
|
||||
|
||||
if let Some(workspace) = target_monitor.focused_workspace() {
|
||||
if !*workspace.tile() {
|
||||
for hwnd in container_hwnds {
|
||||
Window::from(hwnd)
|
||||
.move_to_area(¤t_area, target_monitor.work_area_size())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail!("failed to find a window to move");
|
||||
}
|
||||
|
||||
target_monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
@@ -1718,7 +1999,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 +2022,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 +2182,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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -668,7 +675,12 @@ impl WindowsApi {
|
||||
}
|
||||
|
||||
pub fn center_cursor_in_rect(rect: &Rect) -> Result<()> {
|
||||
Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
|
||||
Self::place_cursor_at_top_left_of_rect(rect)
|
||||
// Self::set_cursor_pos(rect.left + (rect.right / 2), rect.top + (rect.bottom / 2))
|
||||
}
|
||||
|
||||
pub fn place_cursor_at_top_left_of_rect(rect: &Rect) -> Result<()> {
|
||||
Self::set_cursor_pos(rect.left + 15, rect.top + 15)
|
||||
}
|
||||
|
||||
pub fn window_thread_process_id(hwnd: isize) -> (u32, u32) {
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::static_config::WorkspaceConfig;
|
||||
use crate::window::Window;
|
||||
use crate::window::WindowDetails;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::WindowContainerBehaviour;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
@@ -83,6 +84,10 @@ pub struct Workspace {
|
||||
tile: bool,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
apply_window_based_work_area_offset: bool,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
float_override: Option<bool>,
|
||||
}
|
||||
|
||||
impl_ring_elements!(Workspace, Container);
|
||||
@@ -106,6 +111,8 @@ impl Default for Workspace {
|
||||
resize_dimensions: vec![],
|
||||
tile: true,
|
||||
apply_window_based_work_area_offset: true,
|
||||
window_container_behaviour: None,
|
||||
float_override: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +169,14 @@ impl Workspace {
|
||||
config.apply_window_based_work_area_offset.unwrap_or(true),
|
||||
);
|
||||
|
||||
if config.window_container_behaviour.is_some() {
|
||||
self.set_window_container_behaviour(config.window_container_behaviour);
|
||||
}
|
||||
|
||||
if config.float_override.is_some() {
|
||||
self.set_float_override(config.float_override);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -217,23 +232,24 @@ impl Workspace {
|
||||
container.restore();
|
||||
}
|
||||
|
||||
for container in self.containers_mut() {
|
||||
container.restore();
|
||||
if let Some(container) = self.focused_container_mut() {
|
||||
container.focus_window(container.focused_window_idx());
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
window.restore();
|
||||
}
|
||||
|
||||
if let Some(container) = self.focused_container_mut() {
|
||||
container.focus_window(container.focused_window_idx());
|
||||
}
|
||||
|
||||
// Do this here to make sure that an error doesn't stop the restoration of other windows
|
||||
// Maximised windows should always be drawn at the top of the Z order
|
||||
// Maximised windows and floating windows should always be drawn at the top of the Z order
|
||||
// when switching to a workspace
|
||||
if let Some(window) = to_focus {
|
||||
if self.maximized_window().is_none() {
|
||||
if self.maximized_window().is_none() && self.floating_windows().is_empty() {
|
||||
window.focus(mouse_follows_focus)?;
|
||||
} else if let Some(maximized_window) = self.maximized_window() {
|
||||
maximized_window.focus(mouse_follows_focus)?;
|
||||
} else if let Some(floating_window) = self.floating_windows().first() {
|
||||
floating_window.focus(mouse_follows_focus)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,21 +361,7 @@ impl Workspace {
|
||||
for (i, container) in containers.iter_mut().enumerate() {
|
||||
let window_count = container.windows().len();
|
||||
|
||||
if let (Some(window), Some(layout)) =
|
||||
(container.focused_window_mut(), layouts.get_mut(i))
|
||||
{
|
||||
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
|
||||
window.remove_title_bar()?;
|
||||
} else if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
// If a window has been unmaximized via toggle-maximize, this block
|
||||
// will make sure that it is unmaximized via restore_window
|
||||
if window.is_maximized() && !managed_maximized_window {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
|
||||
if let Some(layout) = layouts.get_mut(i) {
|
||||
{
|
||||
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
|
||||
layout.add_padding(border_offset);
|
||||
@@ -376,7 +378,25 @@ impl Workspace {
|
||||
layout.bottom -= total_height;
|
||||
}
|
||||
|
||||
window.set_position(layout, false)?;
|
||||
for window in container.windows() {
|
||||
if container
|
||||
.focused_window()
|
||||
.is_some_and(|w| w.hwnd == window.hwnd)
|
||||
{
|
||||
if should_remove_titlebars && no_titlebar.contains(&window.exe()?) {
|
||||
window.remove_title_bar()?;
|
||||
} else if no_titlebar.contains(&window.exe()?) {
|
||||
window.add_title_bar()?;
|
||||
}
|
||||
|
||||
// If a window has been unmaximized via toggle-maximize, this block
|
||||
// will make sure that it is unmaximized via restore_window
|
||||
if window.is_maximized() && !managed_maximized_window {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
window.set_position(layout, false)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,26 +413,6 @@ impl Workspace {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// focus_changed performs updates in response to the fact that a focus
|
||||
// change event has occurred. The focus change is assumed to be valid, and
|
||||
// should not result in a new focus change - the intent here is to update
|
||||
// focus-reactive elements, such as the stackbar.
|
||||
pub fn focus_changed(&mut self, hwnd: isize) -> Result<()> {
|
||||
if !self.tile() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let containers = self.containers_mut();
|
||||
|
||||
for container in containers.iter_mut() {
|
||||
if let Some(idx) = container.idx_for_window(hwnd) {
|
||||
container.focus_window(idx);
|
||||
container.restore();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reap_orphans(&mut self) -> Result<(usize, usize)> {
|
||||
let mut hwnds = vec![];
|
||||
let mut floating_hwnds = vec![];
|
||||
@@ -439,7 +439,7 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
for window in self.visible_windows_mut().into_iter().flatten() {
|
||||
for window in self.visible_windows().into_iter().flatten() {
|
||||
if !window.is_window() {
|
||||
hwnds.push(window.hwnd);
|
||||
}
|
||||
@@ -605,6 +605,13 @@ impl Workspace {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.containers().is_empty()
|
||||
&& self.maximized_window().is_none()
|
||||
&& self.monocle_container().is_none()
|
||||
&& self.floating_windows().is_empty()
|
||||
}
|
||||
|
||||
pub fn contains_window(&self, hwnd: isize) -> bool {
|
||||
for container in self.containers() {
|
||||
if container.contains_window(hwnd) {
|
||||
@@ -1410,16 +1417,41 @@ impl Workspace {
|
||||
|
||||
pub fn visible_windows(&self) -> Vec<Option<&Window>> {
|
||||
let mut vec = vec![];
|
||||
|
||||
vec.push(self.maximized_window().as_ref());
|
||||
|
||||
if let Some(monocle) = self.monocle_container() {
|
||||
vec.push(monocle.focused_window());
|
||||
}
|
||||
|
||||
for container in self.containers() {
|
||||
vec.push(container.focused_window());
|
||||
}
|
||||
|
||||
for window in self.floating_windows() {
|
||||
vec.push(Some(window));
|
||||
}
|
||||
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn visible_window_details(&self) -> Vec<WindowDetails> {
|
||||
let mut vec: Vec<WindowDetails> = vec![];
|
||||
|
||||
if let Some(maximized) = self.maximized_window() {
|
||||
if let Ok(details) = (*maximized).try_into() {
|
||||
vec.push(details);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(monocle) = self.monocle_container() {
|
||||
if let Some(focused) = monocle.focused_window() {
|
||||
if let Ok(details) = (*focused).try_into() {
|
||||
vec.push(details);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for container in self.containers() {
|
||||
if let Some(focused) = container.focused_window() {
|
||||
if let Ok(details) = (*focused).try_into() {
|
||||
@@ -1428,13 +1460,10 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn visible_windows_mut(&mut self) -> Vec<Option<&mut Window>> {
|
||||
let mut vec = vec![];
|
||||
for container in self.containers_mut() {
|
||||
vec.push(container.focused_window_mut());
|
||||
for window in self.floating_windows() {
|
||||
if let Ok(details) = (*window).try_into() {
|
||||
vec.push(details);
|
||||
}
|
||||
}
|
||||
|
||||
vec
|
||||
|
||||
@@ -88,6 +88,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let mouse_follows_focus = wm.mouse_follows_focus;
|
||||
|
||||
if let Some(monitor) = wm.focused_monitor_mut() {
|
||||
let previous_idx = monitor.focused_workspace_idx();
|
||||
monitor.set_last_focused_workspace(Option::from(previous_idx));
|
||||
monitor.focus_workspace(notification.workspace_idx)?;
|
||||
monitor.load_focused_workspace(mouse_follows_focus)?;
|
||||
}
|
||||
@@ -118,7 +120,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Unblock the border manager
|
||||
ALT_TAB_HWND.store(None);
|
||||
// Send a notification to the border manager to update the borders
|
||||
border_manager::send_notification();
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
[package]
|
||||
name = "komorebic-no-console"
|
||||
version = "0.1.30"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
version = "0.1.31"
|
||||
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
||||
categories = ["cli", "tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.30"
|
||||
authors = ["Jade Iqbal <jadeiqbal@fastmail.com>"]
|
||||
version = "0.1.31"
|
||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||
categories = ["cli", "tiling-window-manager", "windows"]
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -30,7 +28,7 @@ serde_json = { workspace = true }
|
||||
serde_yaml = "0.9"
|
||||
shadow-rs = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
thiserror = "1"
|
||||
thiserror = "2"
|
||||
uds_windows = { workspace = true }
|
||||
which = { workspace = true }
|
||||
win32-display-data = { workspace = true }
|
||||
@@ -38,4 +36,7 @@ windows = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
shadow-rs = { workspace = true }
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(FALSE)'] }
|
||||
@@ -1,9 +1,9 @@
|
||||
fn main() {
|
||||
if std::fs::metadata("applications.yaml").is_err() {
|
||||
let applications_yaml = reqwest::blocking::get(
|
||||
"https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.yaml"
|
||||
if std::fs::metadata("applications.json").is_err() {
|
||||
let applications_json = reqwest::blocking::get(
|
||||
"https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.json"
|
||||
).unwrap().text().unwrap();
|
||||
std::fs::write("applications.yaml", applications_yaml).unwrap();
|
||||
std::fs::write("applications.json", applications_json).unwrap();
|
||||
}
|
||||
|
||||
shadow_rs::new().unwrap();
|
||||
|
||||
@@ -25,7 +25,7 @@ use fs_tail::TailedFile;
|
||||
use komorebi_client::resolve_home_path;
|
||||
use komorebi_client::send_message;
|
||||
use komorebi_client::send_query;
|
||||
use komorebi_client::ApplicationConfiguration;
|
||||
use komorebi_client::ApplicationSpecificConfiguration;
|
||||
use komorebi_client::Notification;
|
||||
use lazy_static::lazy_static;
|
||||
use miette::NamedSource;
|
||||
@@ -160,6 +160,7 @@ gen_enum_subcommand_args! {
|
||||
CycleMoveWorkspaceToMonitor: CycleDirection,
|
||||
Stack: OperationDirection,
|
||||
CycleStack: CycleDirection,
|
||||
CycleStackIndex: CycleDirection,
|
||||
FlipLayout: Axis,
|
||||
ChangeLayout: DefaultLayout,
|
||||
CycleLayout: CycleDirection,
|
||||
@@ -583,7 +584,7 @@ macro_rules! gen_application_target_subcommand_args {
|
||||
}
|
||||
|
||||
gen_application_target_subcommand_args! {
|
||||
FloatRule,
|
||||
IgnoreRule,
|
||||
ManageRule,
|
||||
IdentifyTrayApplication,
|
||||
IdentifyLayeredApplication,
|
||||
@@ -748,6 +749,7 @@ struct AnimationStyle {
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct Start {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(hide = true)]
|
||||
#[clap(short, long = "ffm")]
|
||||
ffm: bool,
|
||||
/// Path to a static configuration JSON file
|
||||
@@ -775,6 +777,9 @@ struct Stop {
|
||||
/// Stop whkd if it is running as a background process
|
||||
#[clap(long)]
|
||||
whkd: bool,
|
||||
/// Stop ahk if it is running as a background process
|
||||
#[clap(long)]
|
||||
ahk: bool,
|
||||
/// Stop komorebi-bar if it is running as a background process
|
||||
#[clap(long)]
|
||||
bar: bool,
|
||||
@@ -844,6 +849,12 @@ struct FormatAppSpecificConfiguration {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct ConvertAppSpecificConfiguration {
|
||||
/// YAML file from which the application-specific configurations should be loaded
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct AltFocusHack {
|
||||
#[clap(value_enum)]
|
||||
@@ -856,6 +867,7 @@ struct EnableAutostart {
|
||||
#[clap(action, short, long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Enable komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(hide = true)]
|
||||
#[clap(short, long = "ffm")]
|
||||
ffm: bool,
|
||||
/// Enable autostart of whkd
|
||||
@@ -974,6 +986,9 @@ enum SubCommand {
|
||||
/// Cycle the focused stack in the specified cycle direction
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleStack(CycleStack),
|
||||
/// Cycle the index of the focused window in the focused stack in the specified cycle direction
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleStackIndex(CycleStackIndex),
|
||||
/// Focus the specified window index in the focused stack
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusStackWindow(FocusStackWindow),
|
||||
@@ -1041,6 +1056,8 @@ enum SubCommand {
|
||||
/// Focus the specified workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusNamedWorkspace(FocusNamedWorkspace),
|
||||
/// Close the focused workspace (must be empty and unnamed)
|
||||
CloseWorkspace,
|
||||
/// Focus the monitor in the given cycle direction
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleMonitor(CycleMonitor),
|
||||
@@ -1089,6 +1106,7 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleLayout(CycleLayout),
|
||||
/// Load a custom layout from file for the focused workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
LoadCustomLayout(LoadCustomLayout),
|
||||
/// Flip the layout on the focused workspace (BSP only)
|
||||
@@ -1133,9 +1151,11 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
NamedWorkspaceLayout(NamedWorkspaceLayout),
|
||||
/// Set a custom layout for the specified workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
WorkspaceCustomLayout(WorkspaceCustomLayout),
|
||||
/// Set a custom layout for the specified workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
NamedWorkspaceCustomLayout(NamedWorkspaceCustomLayout),
|
||||
/// Add a dynamic layout rule for the specified workspace
|
||||
@@ -1145,9 +1165,11 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
NamedWorkspaceLayoutRule(NamedWorkspaceLayoutRule),
|
||||
/// Add a dynamic custom layout for the specified workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
WorkspaceCustomLayoutRule(WorkspaceCustomLayoutRule),
|
||||
/// Add a dynamic custom layout for the specified workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
NamedWorkspaceCustomLayoutRule(NamedWorkspaceCustomLayoutRule),
|
||||
/// Clear all dynamic layout rules for the specified workspace
|
||||
@@ -1167,6 +1189,16 @@ enum SubCommand {
|
||||
WorkspaceName(WorkspaceName),
|
||||
/// Toggle the behaviour for new windows (stacking or dynamic tiling)
|
||||
ToggleWindowContainerBehaviour,
|
||||
/// Enable or disable float override, which makes it so every new window opens in floating mode
|
||||
ToggleFloatOverride,
|
||||
/// 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.
|
||||
ToggleWorkspaceWindowContainerBehaviour,
|
||||
/// 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.
|
||||
ToggleWorkspaceFloatOverride,
|
||||
/// Toggle window tiling on the focused workspace
|
||||
TogglePause,
|
||||
/// Toggle window tiling on the focused workspace
|
||||
@@ -1208,9 +1240,10 @@ enum SubCommand {
|
||||
/// Set the operation behaviour when the focused window is not managed
|
||||
#[clap(arg_required_else_help = true)]
|
||||
UnmanagedWindowOperationBehaviour(UnmanagedWindowOperationBehaviour),
|
||||
/// Add a rule to always float the specified application
|
||||
/// Add a rule to ignore the specified application
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FloatRule(FloatRule),
|
||||
#[clap(alias = "float-rule")]
|
||||
IgnoreRule(IgnoreRule),
|
||||
/// Add a rule to always manage the specified application
|
||||
#[clap(arg_required_else_help = true)]
|
||||
ManageRule(ManageRule),
|
||||
@@ -1295,9 +1328,11 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
AnimationStyle(AnimationStyle),
|
||||
/// Enable or disable focus follows mouse for the operating system
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
FocusFollowsMouse(FocusFollowsMouse),
|
||||
/// Toggle focus follows mouse for the operating system
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
ToggleFocusFollowsMouse(ToggleFocusFollowsMouse),
|
||||
/// Enable or disable mouse follows focus on all workspaces
|
||||
@@ -1313,14 +1348,19 @@ enum SubCommand {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "pwsh-asc")]
|
||||
PwshAppSpecificConfiguration(PwshAppSpecificConfiguration),
|
||||
/// Format a YAML file for use with the 'ahk-app-specific-configuration' command
|
||||
/// Convert a v1 ASC YAML file to a v2 ASC JSON file
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "convert-asc")]
|
||||
ConvertAppSpecificConfiguration(ConvertAppSpecificConfiguration),
|
||||
/// Format a YAML file for use with the 'app-specific-configuration' command
|
||||
#[clap(arg_required_else_help = true)]
|
||||
#[clap(alias = "fmt-asc")]
|
||||
#[clap(hide = true)]
|
||||
FormatAppSpecificConfiguration(FormatAppSpecificConfiguration),
|
||||
/// Fetch the latest version of applications.yaml from komorebi-application-specific-configuration
|
||||
/// Fetch the latest version of applications.json from komorebi-application-specific-configuration
|
||||
#[clap(alias = "fetch-asc")]
|
||||
FetchAppSpecificConfiguration,
|
||||
/// Generate a JSON Schema for applications.yaml
|
||||
/// Generate a JSON Schema for applications.json
|
||||
#[clap(alias = "asc-schema")]
|
||||
ApplicationSpecificConfigurationSchema,
|
||||
/// Generate a JSON Schema of subscription notifications
|
||||
@@ -1378,6 +1418,14 @@ fn main() -> Result<()> {
|
||||
"docgen",
|
||||
"alt-focus-hack",
|
||||
"identify-border-overflow-application",
|
||||
"load-custom-layout",
|
||||
"workspace-custom-layout",
|
||||
"named-workspace-custom-layout",
|
||||
"workspace-custom-layout-rule",
|
||||
"named-workspace-custom-layout-rule",
|
||||
"focus-follows-mouse",
|
||||
"toggle-focus-follows-mouse",
|
||||
"format-app-specific-configuration",
|
||||
];
|
||||
|
||||
for cmd in subcommands {
|
||||
@@ -1410,13 +1458,13 @@ fn main() -> Result<()> {
|
||||
std::fs::write(HOME_DIR.join("komorebi.json"), komorebi_json)?;
|
||||
std::fs::write(HOME_DIR.join("komorebi.bar.json"), komorebi_bar_json)?;
|
||||
|
||||
let applications_yaml = include_str!("../applications.yaml");
|
||||
std::fs::write(HOME_DIR.join("applications.yaml"), applications_yaml)?;
|
||||
let applications_json = include_str!("../applications.json");
|
||||
std::fs::write(HOME_DIR.join("applications.json"), applications_json)?;
|
||||
|
||||
let whkdrc = include_str!("../../docs/whkdrc.sample");
|
||||
std::fs::write(WHKD_CONFIG_DIR.join("whkdrc"), whkdrc)?;
|
||||
|
||||
println!("Example komorebi.json, komorebi.bar.json, whkdrc and latest applications.yaml files created");
|
||||
println!("Example komorebi.json, komorebi.bar.json, whkdrc and latest applications.json files created");
|
||||
println!("You can now run komorebic start --whkd --bar");
|
||||
}
|
||||
SubCommand::EnableAutostart(args) => {
|
||||
@@ -1517,26 +1565,15 @@ fn main() -> Result<()> {
|
||||
|
||||
println!("Found komorebi.json; this file can be passed to the start command with the --config flag\n");
|
||||
|
||||
if let Ok(config) = &parsed_config {
|
||||
if let Some(asc_path) = config.get("app_specific_configuration_path") {
|
||||
let mut normalized_asc_path = asc_path
|
||||
.to_string()
|
||||
.replace(
|
||||
"$Env:USERPROFILE",
|
||||
&dirs::home_dir().unwrap().to_string_lossy(),
|
||||
)
|
||||
.replace('"', "")
|
||||
.replace('\\', "/");
|
||||
|
||||
if let Ok(komorebi_config_home) = std::env::var("KOMOREBI_CONFIG_HOME") {
|
||||
normalized_asc_path = normalized_asc_path
|
||||
.replace("$Env:KOMOREBI_CONFIG_HOME", &komorebi_config_home)
|
||||
.replace('"', "")
|
||||
.replace('\\', "/");
|
||||
if let Ok(config) = StaticConfig::read(&static_config) {
|
||||
match config.app_specific_configuration_path {
|
||||
None => {
|
||||
println!("Application specific configuration file path has not been set. Try running 'komorebic fetch-asc'\n");
|
||||
}
|
||||
|
||||
if !Path::exists(Path::new(&normalized_asc_path)) {
|
||||
println!("Application specific configuration file path '{normalized_asc_path}' does not exist. Try running 'komorebic fetch-asc'\n");
|
||||
Some(path) => {
|
||||
if !Path::exists(Path::new(&path)) {
|
||||
println!("Application specific configuration file path '{}' does not exist. Try running 'komorebic fetch-asc'\n", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1546,6 +1583,12 @@ fn main() -> Result<()> {
|
||||
// errors
|
||||
let _ = serde_json::from_str::<StaticConfig>(&config_source)?;
|
||||
|
||||
let path = resolve_home_path(static_config)?;
|
||||
let raw = std::fs::read_to_string(path)?;
|
||||
StaticConfig::aliases(&raw);
|
||||
StaticConfig::deprecated(&raw);
|
||||
StaticConfig::end_of_life(&raw);
|
||||
|
||||
if config_whkd.exists() {
|
||||
println!("Found {}; key bindings will be loaded from here when whkd is started, and you can start it automatically using the --whkd flag\n", config_whkd.to_string_lossy());
|
||||
} else {
|
||||
@@ -2010,7 +2053,7 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
|
||||
let script = format!(
|
||||
r#"
|
||||
Start-Process '{ahk}' '{config}' -WindowStyle hidden
|
||||
Start-Process '"{ahk}"' '"{config}"' -WindowStyle hidden
|
||||
"#,
|
||||
config = config_ahk.display()
|
||||
);
|
||||
@@ -2025,32 +2068,7 @@ if (!(Get-Process whkd -ErrorAction SilentlyContinue))
|
||||
}
|
||||
}
|
||||
|
||||
if arg.bar {
|
||||
let script = r"
|
||||
if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
|
||||
{
|
||||
Start-Process komorebi-bar -WindowStyle hidden
|
||||
}
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nThank you for using komorebi!\n");
|
||||
println!("* Become a sponsor https://github.com/sponsors/LGUG2Z - Even $1/month makes a big difference");
|
||||
println!(
|
||||
"* Subscribe to https://youtube.com/@LGUG2Z - Live dev videos and feature previews"
|
||||
);
|
||||
println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, share your desktops");
|
||||
println!("* Read the docs https://lgug2z.github.io/komorebi - Quickly search through all komorebic commands");
|
||||
|
||||
let static_config = arg.config.map_or_else(
|
||||
let static_config = arg.config.clone().map_or_else(
|
||||
|| {
|
||||
let komorebi_json = HOME_DIR.join("komorebi.json");
|
||||
if komorebi_json.is_file() {
|
||||
@@ -2062,11 +2080,79 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
|
||||
Option::from,
|
||||
);
|
||||
|
||||
if let Some(config) = static_config {
|
||||
if arg.bar {
|
||||
if let Some(config) = &static_config {
|
||||
let mut config = StaticConfig::read(config)?;
|
||||
if let Some(display_bar_configurations) = &mut config.bar_configurations {
|
||||
for config_file_path in &mut *display_bar_configurations {
|
||||
let script = r#"Start-Process 'komorebi-bar' '--config "CONFIGFILE"' -WindowStyle hidden"#
|
||||
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
|
||||
|
||||
match powershell_script::run(&script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let script = r"
|
||||
if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
|
||||
{
|
||||
Start-Process komorebi-bar -WindowStyle hidden
|
||||
}
|
||||
";
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nThank you for using komorebi!\n");
|
||||
println!("# Sponsorship");
|
||||
println!("* Become a sponsor https://github.com/sponsors/LGUG2Z - $5/month makes a big difference");
|
||||
println!("* Leave a tip https://ko-fi.com/lgug2z - An alternative to GitHub Sponsors");
|
||||
println!(
|
||||
"* Subscribe to https://youtube.com/@LGUG2Z - Development videos, feature previews and release overviews"
|
||||
);
|
||||
println!("\n# Community");
|
||||
println!("* Join the Discord https://discord.gg/mGkn66PHkx - Chat, ask questions, share your desktops");
|
||||
println!("* Explore the Awesome Komorebi list https://github.com/LGUG2Z/awesome-komorebi - Projects in the komorebi ecosystem");
|
||||
println!("\n# Documentation");
|
||||
println!("* Read the docs https://lgug2z.github.io/komorebi - Quickly search through all komorebic commands");
|
||||
|
||||
let bar_config = arg.config.map_or_else(
|
||||
|| {
|
||||
let bar_json = HOME_DIR.join("komorebi.bar.json");
|
||||
if bar_json.is_file() {
|
||||
Option::from(bar_json)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
Option::from,
|
||||
);
|
||||
|
||||
if let Some(config) = &static_config {
|
||||
let path = resolve_home_path(config)?;
|
||||
let raw = std::fs::read_to_string(path)?;
|
||||
StaticConfig::aliases(&raw);
|
||||
StaticConfig::deprecated(&raw);
|
||||
StaticConfig::end_of_life(&raw);
|
||||
}
|
||||
|
||||
if bar_config.is_some() {
|
||||
let output = Command::new("komorebi-bar.exe").arg("--aliases").output()?;
|
||||
let stdout = String::from_utf8(output.stdout)?;
|
||||
println!("{stdout}");
|
||||
}
|
||||
}
|
||||
SubCommand::Stop(arg) => {
|
||||
@@ -2098,6 +2184,35 @@ Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
if arg.ahk {
|
||||
let script = r#"
|
||||
if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
||||
(Get-CimInstance Win32_Process | Where-Object {
|
||||
($_.CommandLine -like '*komorebi.ahk"') -and
|
||||
($_.Name -in @('AutoHotkey.exe', 'AutoHotkey64.exe', 'AutoHotkey32.exe', 'AutoHotkeyUX.exe'))
|
||||
} | Select-Object -First 1) | ForEach-Object {
|
||||
Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue
|
||||
}
|
||||
} else {
|
||||
(Get-WmiObject Win32_Process | Where-Object {
|
||||
($_.CommandLine -like '*komorebi.ahk"') -and
|
||||
($_.Name -in @('AutoHotkey.exe', 'AutoHotkey64.exe', 'AutoHotkey32.exe', 'AutoHotkeyUX.exe'))
|
||||
} | Select-Object -First 1) | ForEach-Object {
|
||||
Stop-Process -Id $_.ProcessId -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
match powershell_script::run(script) {
|
||||
Ok(_) => {
|
||||
println!("{script}");
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send_message(&SocketMessage::Stop)?;
|
||||
let mut system = sysinfo::System::new_all();
|
||||
system.refresh_processes(ProcessesToUpdate::All);
|
||||
@@ -2128,8 +2243,8 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
SubCommand::FloatRule(arg) => {
|
||||
send_message(&SocketMessage::FloatRule(arg.identifier, arg.id))?;
|
||||
SubCommand::IgnoreRule(arg) => {
|
||||
send_message(&SocketMessage::IgnoreRule(arg.identifier, arg.id))?;
|
||||
}
|
||||
SubCommand::ManageRule(arg) => {
|
||||
send_message(&SocketMessage::ManageRule(arg.identifier, arg.id))?;
|
||||
@@ -2194,6 +2309,9 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
SubCommand::CycleStack(arg) => {
|
||||
send_message(&SocketMessage::CycleStack(arg.cycle_direction))?;
|
||||
}
|
||||
SubCommand::CycleStackIndex(arg) => {
|
||||
send_message(&SocketMessage::CycleStackIndex(arg.cycle_direction))?;
|
||||
}
|
||||
SubCommand::ChangeLayout(arg) => {
|
||||
send_message(&SocketMessage::ChangeLayout(arg.default_layout))?;
|
||||
}
|
||||
@@ -2229,6 +2347,9 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
SubCommand::FocusNamedWorkspace(arg) => {
|
||||
send_message(&SocketMessage::FocusNamedWorkspace(arg.workspace))?;
|
||||
}
|
||||
SubCommand::CloseWorkspace => {
|
||||
send_message(&SocketMessage::CloseWorkspace)?;
|
||||
}
|
||||
SubCommand::CycleMonitor(arg) => {
|
||||
send_message(&SocketMessage::CycleFocusMonitor(arg.cycle_direction))?;
|
||||
}
|
||||
@@ -2443,6 +2564,15 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
SubCommand::ToggleWindowContainerBehaviour => {
|
||||
send_message(&SocketMessage::ToggleWindowContainerBehaviour)?;
|
||||
}
|
||||
SubCommand::ToggleFloatOverride => {
|
||||
send_message(&SocketMessage::ToggleFloatOverride)?;
|
||||
}
|
||||
SubCommand::ToggleWorkspaceWindowContainerBehaviour => {
|
||||
send_message(&SocketMessage::ToggleWorkspaceWindowContainerBehaviour)?;
|
||||
}
|
||||
SubCommand::ToggleWorkspaceFloatOverride => {
|
||||
send_message(&SocketMessage::ToggleWorkspaceFloatOverride)?;
|
||||
}
|
||||
SubCommand::WindowHidingBehaviour(arg) => {
|
||||
send_message(&SocketMessage::WindowHidingBehaviour(arg.hiding_behaviour))?;
|
||||
}
|
||||
@@ -2513,6 +2643,14 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
generated_config.display()
|
||||
);
|
||||
}
|
||||
SubCommand::ConvertAppSpecificConfiguration(arg) => {
|
||||
let file_path = resolve_home_path(arg.path)?;
|
||||
let content = std::fs::read_to_string(&file_path)?;
|
||||
let mut asc = ApplicationConfigurationGenerator::load(&content)?;
|
||||
asc.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let v2 = ApplicationSpecificConfiguration::from(asc);
|
||||
println!("{}", serde_json::to_string_pretty(&v2)?);
|
||||
}
|
||||
SubCommand::FormatAppSpecificConfiguration(arg) => {
|
||||
let file_path = resolve_home_path(arg.path)?;
|
||||
let content = std::fs::read_to_string(&file_path)?;
|
||||
@@ -2529,10 +2667,10 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
println!("File successfully formatted for PRs to https://github.com/LGUG2Z/komorebi-application-specific-configuration");
|
||||
}
|
||||
SubCommand::FetchAppSpecificConfiguration => {
|
||||
let content = reqwest::blocking::get("https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.yaml")?
|
||||
let content = reqwest::blocking::get("https://raw.githubusercontent.com/LGUG2Z/komorebi-application-specific-configuration/master/applications.json")?
|
||||
.text()?;
|
||||
|
||||
let output_file = HOME_DIR.join("applications.yaml");
|
||||
let output_file = HOME_DIR.join("applications.json");
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
@@ -2542,14 +2680,14 @@ Stop-Process -Name:komorebi -ErrorAction SilentlyContinue
|
||||
|
||||
file.write_all(content.as_bytes())?;
|
||||
|
||||
println!("Latest version of applications.yaml from https://github.com/LGUG2Z/komorebi-application-specific-configuration downloaded\n");
|
||||
println!("Latest version of applications.json from https://github.com/LGUG2Z/komorebi-application-specific-configuration downloaded\n");
|
||||
println!(
|
||||
"You can add this to your komorebi.json static configuration file like this: \n\n\"app_specific_configuration_path\": \"{}\"",
|
||||
output_file.display()
|
||||
output_file.display().to_string().replace("\\", "/")
|
||||
);
|
||||
}
|
||||
SubCommand::ApplicationSpecificConfigurationSchema => {
|
||||
let asc = schema_for!(Vec<ApplicationConfiguration>);
|
||||
let asc = schema_for!(ApplicationSpecificConfiguration);
|
||||
let schema = serde_json::to_string_pretty(&asc)?;
|
||||
println!("{schema}");
|
||||
}
|
||||
|
||||
21
mkdocs.yml
21
mkdocs.yml
@@ -57,6 +57,7 @@ nav:
|
||||
- Troubleshooting: troubleshooting.md
|
||||
- Common workflows:
|
||||
- common-workflows/komorebi-config-home.md
|
||||
- common-workflows/autostart.md
|
||||
- common-workflows/animations.md
|
||||
- common-workflows/autohotkey.md
|
||||
- common-workflows/borders.md
|
||||
@@ -64,14 +65,12 @@ nav:
|
||||
- common-workflows/remove-gaps.md
|
||||
- common-workflows/ignore-windows.md
|
||||
- common-workflows/force-manage-windows.md
|
||||
- common-workflows/floating-applications.md
|
||||
- common-workflows/tray-and-multi-window-applications.md
|
||||
- common-workflows/focus-follows-mouse.md
|
||||
- common-workflows/mouse-follows-focus.md
|
||||
- common-workflows/custom-layouts.md
|
||||
- common-workflows/dynamic-layout-switching.md
|
||||
- common-workflows/set-display-index.md
|
||||
- Release notes:
|
||||
- release/v0-1-22.md
|
||||
- common-workflows/multiple-bar-instances.md
|
||||
- Configuration reference: https://komorebi.lgug2z.com/schema
|
||||
- Bar reference: https://komorebi-bar.lgug2z.com/schema
|
||||
- CLI reference:
|
||||
@@ -146,7 +145,6 @@ nav:
|
||||
- cli/adjust-workspace-padding.md
|
||||
- cli/change-layout.md
|
||||
- cli/cycle-layout.md
|
||||
- cli/load-custom-layout.md
|
||||
- cli/flip-layout.md
|
||||
- cli/promote.md
|
||||
- cli/promote-focus.md
|
||||
@@ -162,18 +160,17 @@ nav:
|
||||
- cli/named-workspace-padding.md
|
||||
- cli/workspace-layout.md
|
||||
- cli/named-workspace-layout.md
|
||||
- cli/workspace-custom-layout.md
|
||||
- cli/named-workspace-custom-layout.md
|
||||
- cli/workspace-layout-rule.md
|
||||
- cli/named-workspace-layout-rule.md
|
||||
- cli/workspace-custom-layout-rule.md
|
||||
- cli/named-workspace-custom-layout-rule.md
|
||||
- cli/clear-workspace-layout-rules.md
|
||||
- cli/clear-named-workspace-layout-rules.md
|
||||
- cli/workspace-tiling.md
|
||||
- cli/named-workspace-tiling.md
|
||||
- cli/workspace-name.md
|
||||
- cli/toggle-window-container-behaviour.md
|
||||
- cli/toggle-float-override.md
|
||||
- cli/toggle-workspace-window-container-behaviour.md
|
||||
- cli/toggle-workspace-float-override.md
|
||||
- cli/toggle-pause.md
|
||||
- cli/toggle-tiling.md
|
||||
- cli/toggle-float.md
|
||||
@@ -190,7 +187,7 @@ nav:
|
||||
- cli/cross-monitor-move-behaviour.md
|
||||
- cli/toggle-cross-monitor-move-behaviour.md
|
||||
- cli/unmanaged-window-operation-behaviour.md
|
||||
- cli/float-rule.md
|
||||
- cli/ignore-rule.md
|
||||
- cli/manage-rule.md
|
||||
- cli/initial-workspace-rule.md
|
||||
- cli/initial-named-workspace-rule.md
|
||||
@@ -217,13 +214,11 @@ nav:
|
||||
- cli/animation-duration.md
|
||||
- cli/animation-fps.md
|
||||
- cli/animation-style.md
|
||||
- cli/focus-follows-mouse.md
|
||||
- cli/toggle-focus-follows-mouse.md
|
||||
- cli/mouse-follows-focus.md
|
||||
- cli/toggle-mouse-follows-focus.md
|
||||
- cli/ahk-app-specific-configuration.md
|
||||
- cli/pwsh-app-specific-configuration.md
|
||||
- cli/format-app-specific-configuration.md
|
||||
- cli/convert-app-specific-configuration.md
|
||||
- cli/fetch-app-specific-configuration.md
|
||||
- cli/application-specific-configuration-schema.md
|
||||
- cli/notification-schema.md
|
||||
|
||||
143
schema.asc.json
143
schema.asc.json
@@ -1,44 +1,11 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Array_of_ApplicationConfiguration",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ApplicationConfiguration"
|
||||
"title": "ApplicationSpecificConfiguration",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/AscApplicationRulesOrSchema"
|
||||
},
|
||||
"definitions": {
|
||||
"ApplicationConfiguration": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"float_identifiers": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"identifier": {
|
||||
"$ref": "#/definitions/IdWithIdentifier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/ApplicationOptions"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApplicationIdentifier": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -48,14 +15,100 @@
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"ApplicationOptions": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"object_name_change",
|
||||
"layered",
|
||||
"tray_and_multi_window",
|
||||
"force",
|
||||
"border_overflow"
|
||||
"AscApplicationRules": {
|
||||
"description": "Rules that determine how an application is handled",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"floating": {
|
||||
"description": "Rules to manage specific windows as floating windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"ignore": {
|
||||
"description": "Rules to ignore specific windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"layered": {
|
||||
"description": "Rules to identify applications which have the `WS_EX_LAYERED` Extended Window Style",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"manage": {
|
||||
"description": "Rules to forcibly manage specific windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"object_name_change": {
|
||||
"description": "Rules to identify applications which send the `EVENT_OBJECT_NAMECHANGE` event on launch",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"slow_application": {
|
||||
"description": "Rules to identify applications which are slow to send initial event notifications",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"transparency_ignore": {
|
||||
"description": "Rules to ignore specific windows from the transparency feature",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
},
|
||||
"tray_and_multi_window": {
|
||||
"description": "Rules to identify applications which minimize to the tray or have multiple windows",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/MatchingRule"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AscApplicationRulesOrSchema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AscApplicationRules"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"IdWithIdentifier": {
|
||||
|
||||
820
schema.bar.json
820
schema.bar.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "KomobarConfig",
|
||||
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.30`",
|
||||
"description": "The `komorebi.bar.json` configuration file reference for `v0.1.31`",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"left_widgets",
|
||||
@@ -19,7 +19,7 @@
|
||||
"format": "float"
|
||||
},
|
||||
"frame": {
|
||||
"description": "Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)",
|
||||
"description": "Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"inner_margin"
|
||||
@@ -47,6 +47,203 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"grouping": {
|
||||
"description": "Visual grouping for widgets",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "No grouping is applied",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Widgets are grouped as a whole",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Bar"
|
||||
]
|
||||
},
|
||||
"rounding": {
|
||||
"description": "Rounding values for the 4 corners. Can be a single or 4 values.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "All 4 corners are the same",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"description": "All 4 corners are custom. Order: NW, NE, SW, SE",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"maxItems": 4,
|
||||
"minItems": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"style": {
|
||||
"description": "Styles for the grouping",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Default"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A black shadow is added under the default group",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DefaultWithShadow"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"transparency_alpha": {
|
||||
"description": "Alpha value for the color transparency [[0-255]] (default: 200)",
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Widgets are grouped by alignment",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Alignment"
|
||||
]
|
||||
},
|
||||
"rounding": {
|
||||
"description": "Rounding values for the 4 corners. Can be a single or 4 values.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "All 4 corners are the same",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"description": "All 4 corners are custom. Order: NW, NE, SW, SE",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"maxItems": 4,
|
||||
"minItems": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"style": {
|
||||
"description": "Styles for the grouping",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Default"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A black shadow is added under the default group",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DefaultWithShadow"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"transparency_alpha": {
|
||||
"description": "Alpha value for the color transparency [[0-255]] (default: 200)",
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Widgets are grouped individually",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Widget"
|
||||
]
|
||||
},
|
||||
"rounding": {
|
||||
"description": "Rounding values for the 4 corners. Can be a single or 4 values.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "All 4 corners are the same",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
{
|
||||
"description": "All 4 corners are custom. Order: NW, NE, SW, SE",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"maxItems": 4,
|
||||
"minItems": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"style": {
|
||||
"description": "Styles for the grouping",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Default"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A black shadow is added under the default group",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DefaultWithShadow"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"transparency_alpha": {
|
||||
"description": "Alpha value for the color transparency [[0-255]] (default: 200)",
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"left_widgets": {
|
||||
"description": "Left side widgets (ordered left-to-right)",
|
||||
"type": "array",
|
||||
@@ -73,6 +270,99 @@
|
||||
"enable": {
|
||||
"description": "Enable the Battery widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Cpu"
|
||||
],
|
||||
"properties": {
|
||||
"Cpu": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"data_refresh_interval": {
|
||||
"description": "Data refresh interval (default: 10 seconds)",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"enable": {
|
||||
"description": "Enable the Cpu widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,6 +431,39 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,6 +599,39 @@
|
||||
"enable": {
|
||||
"description": "Enable the Memory widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,6 +662,39 @@
|
||||
"description": "Enable the Network widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"network_activity_fill_characters": {
|
||||
"description": "Characters to reserve for network activity data",
|
||||
"type": "integer",
|
||||
@@ -346,6 +735,39 @@
|
||||
"enable": {
|
||||
"description": "Enable the Storage widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,6 +822,39 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,6 +916,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"description": "Bar positioning options",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"end": {
|
||||
"description": "The desired size of the bar from the starting position (usually monitor width x desired height)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"description": "X coordinate",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"y": {
|
||||
"description": "Y coordinate",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
"start": {
|
||||
"description": "The desired starting position of the bar (0,0 = top left of the screen)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"description": "X coordinate",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"y": {
|
||||
"description": "Y coordinate",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"right_widgets": {
|
||||
"description": "Right side widgets (ordered left-to-right)",
|
||||
"type": "array",
|
||||
@@ -487,6 +988,99 @@
|
||||
"enable": {
|
||||
"description": "Enable the Battery widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Cpu"
|
||||
],
|
||||
"properties": {
|
||||
"Cpu": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"data_refresh_interval": {
|
||||
"description": "Data refresh interval (default: 10 seconds)",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"enable": {
|
||||
"description": "Enable the Cpu widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -555,6 +1149,39 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -690,6 +1317,39 @@
|
||||
"enable": {
|
||||
"description": "Enable the Memory widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -720,6 +1380,39 @@
|
||||
"description": "Enable the Network widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"network_activity_fill_characters": {
|
||||
"description": "Characters to reserve for network activity data",
|
||||
"type": "integer",
|
||||
@@ -760,6 +1453,39 @@
|
||||
"enable": {
|
||||
"description": "Enable the Storage widget",
|
||||
"type": "boolean"
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -814,6 +1540,39 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"label_prefix": {
|
||||
"description": "Display label prefix",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Show no prefix",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Icon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Show an icon and text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"IconAndText"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -866,6 +1625,7 @@
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Frappe",
|
||||
@@ -912,6 +1672,7 @@
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"3024",
|
||||
@@ -1195,51 +1956,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewport": {
|
||||
"description": "Viewport options (see: https://docs.rs/egui/latest/egui/viewport/struct.ViewportBuilder.html)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inner_size": {
|
||||
"description": "The desired size of the bar from the starting position (usually monitor width x desired height)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"description": "X coordinate",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"y": {
|
||||
"description": "Y coordinate",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"description": "The desired starting position of the bar (0,0 = top left of the screen)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"description": "X coordinate",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"y": {
|
||||
"description": "Y coordinate",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"transparency_alpha": {
|
||||
"description": "Alpha value for the color transparency [[0-255]] (default: 200)",
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"widget_spacing": {
|
||||
"description": "Spacing between widgets (default: 10.0)",
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
531
schema.json
531
schema.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "StaticConfig",
|
||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.30`",
|
||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.31`",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation": {
|
||||
@@ -66,9 +66,16 @@
|
||||
}
|
||||
},
|
||||
"app_specific_configuration_path": {
|
||||
"description": "Path to applications.yaml from komorebi-application-specific-configurations (default: None)",
|
||||
"description": "Path to applications.json from komorebi-application-specific-configurations (default: None)",
|
||||
"type": "string"
|
||||
},
|
||||
"bar_configurations": {
|
||||
"description": "Komorebi status bar configuration files for multiple instances on different monitors",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"border": {
|
||||
"description": "Display an active window border (default: false)",
|
||||
"type": "boolean"
|
||||
@@ -77,6 +84,45 @@
|
||||
"description": "Active window border colours for different container types",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"floating": {
|
||||
"description": "Border colour when the container is in floating mode",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Colour represented as RGB",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"b",
|
||||
"g",
|
||||
"r"
|
||||
],
|
||||
"properties": {
|
||||
"b": {
|
||||
"description": "Blue",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"g": {
|
||||
"description": "Green",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"r": {
|
||||
"description": "Red",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Colour represented as Hex",
|
||||
"type": "string",
|
||||
"format": "color-hex"
|
||||
}
|
||||
]
|
||||
},
|
||||
"monocle": {
|
||||
"description": "Border colour when the container is in monocle mode",
|
||||
"anyOf": [
|
||||
@@ -445,8 +491,12 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"float_rules": {
|
||||
"description": "Individual window floating rules",
|
||||
"float_override": {
|
||||
"description": "Enable or disable float override, which makes it so every new window opens in floating mode (default: false)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"floating_applications": {
|
||||
"description": "Identify applications which should be managed as floating windows",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
@@ -529,7 +579,7 @@
|
||||
}
|
||||
},
|
||||
"focus_follows_mouse": {
|
||||
"description": "Determine focus follows mouse implementation (default: None)",
|
||||
"description": "END OF LIFE FEATURE: Determine focus follows mouse implementation (default: None)",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "A custom FFM implementation (slightly more CPU-intensive)",
|
||||
@@ -579,6 +629,89 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ignore_rules": {
|
||||
"description": "Individual window floating rules",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex",
|
||||
"DoesNotEndWith",
|
||||
"DoesNotStartWith",
|
||||
"DoesNotEqual",
|
||||
"DoesNotContain"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex",
|
||||
"DoesNotEndWith",
|
||||
"DoesNotStartWith",
|
||||
"DoesNotEqual",
|
||||
"DoesNotContain"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"invisible_borders": {
|
||||
"description": "DEPRECATED from v0.1.22: no longer required",
|
||||
"type": "object",
|
||||
@@ -919,54 +1052,101 @@
|
||||
"format": "int32"
|
||||
},
|
||||
"custom_layout": {
|
||||
"description": "Custom Layout (default: None)",
|
||||
"description": "END OF LIFE FEATURE: Custom Layout (default: None)",
|
||||
"type": "string"
|
||||
},
|
||||
"custom_layout_rules": {
|
||||
"description": "Layout rules (default: None)",
|
||||
"description": "END OF LIFE FEATURE: Custom layout rules (default: None)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"float_override": {
|
||||
"description": "Enable or disable float override, which makes it so every new window opens in floating mode (default: false)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"initial_workspace_rules": {
|
||||
"description": "Initial workspace application rules",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex",
|
||||
"DoesNotEndWith",
|
||||
"DoesNotStartWith",
|
||||
"DoesNotEqual",
|
||||
"DoesNotContain"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex",
|
||||
"DoesNotEndWith",
|
||||
"DoesNotStartWith",
|
||||
"DoesNotEqual",
|
||||
"DoesNotContain"
|
||||
]
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex",
|
||||
"DoesNotEndWith",
|
||||
"DoesNotStartWith",
|
||||
"DoesNotEqual",
|
||||
"DoesNotContain"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -1004,6 +1184,25 @@
|
||||
"description": "Name",
|
||||
"type": "string"
|
||||
},
|
||||
"window_container_behaviour": {
|
||||
"description": "Determine what happens when a new window is opened (default: Create)",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Create a new container for each new window",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Create"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Append new windows to the focused window container",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Append"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspace_padding": {
|
||||
"description": "Container padding (default: global)",
|
||||
"type": "integer",
|
||||
@@ -1013,40 +1212,83 @@
|
||||
"description": "Permanent workspace application rules",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex",
|
||||
"DoesNotEndWith",
|
||||
"DoesNotStartWith",
|
||||
"DoesNotEqual",
|
||||
"DoesNotContain"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex",
|
||||
"DoesNotEndWith",
|
||||
"DoesNotStartWith",
|
||||
"DoesNotEqual",
|
||||
"DoesNotContain"
|
||||
]
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex",
|
||||
"DoesNotEndWith",
|
||||
"DoesNotStartWith",
|
||||
"DoesNotEqual",
|
||||
"DoesNotContain"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1147,6 +1389,95 @@
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"slow_application_compensation_time": {
|
||||
"description": "How long to wait when compensating for slow applications, in milliseconds (default: 20)",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"slow_application_identifiers": {
|
||||
"description": "Identify applications which are slow to send initial event notifications",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex",
|
||||
"DoesNotEndWith",
|
||||
"DoesNotStartWith",
|
||||
"DoesNotEqual",
|
||||
"DoesNotContain"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Exe",
|
||||
"Class",
|
||||
"Title",
|
||||
"Path"
|
||||
]
|
||||
},
|
||||
"matching_strategy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Legacy",
|
||||
"Equals",
|
||||
"StartsWith",
|
||||
"EndsWith",
|
||||
"Contains",
|
||||
"Regex",
|
||||
"DoesNotEndWith",
|
||||
"DoesNotStartWith",
|
||||
"DoesNotEqual",
|
||||
"DoesNotContain"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"stackbar": {
|
||||
"description": "Stackbar configuration options",
|
||||
"type": "object",
|
||||
@@ -1355,6 +1686,38 @@
|
||||
"Crust"
|
||||
]
|
||||
},
|
||||
"floating_border": {
|
||||
"description": "Border colour when the window is floating (default: Yellow)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Rosewater",
|
||||
"Flamingo",
|
||||
"Pink",
|
||||
"Mauve",
|
||||
"Red",
|
||||
"Maroon",
|
||||
"Peach",
|
||||
"Yellow",
|
||||
"Green",
|
||||
"Teal",
|
||||
"Sky",
|
||||
"Sapphire",
|
||||
"Blue",
|
||||
"Lavender",
|
||||
"Text",
|
||||
"Subtext1",
|
||||
"Subtext0",
|
||||
"Overlay2",
|
||||
"Overlay1",
|
||||
"Overlay0",
|
||||
"Surface2",
|
||||
"Surface1",
|
||||
"Surface0",
|
||||
"Base",
|
||||
"Mantle",
|
||||
"Crust"
|
||||
]
|
||||
},
|
||||
"monocle_border": {
|
||||
"description": "Border colour when the container is in monocle mode (default: Pink)",
|
||||
"type": "string",
|
||||
@@ -1388,7 +1751,7 @@
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the Catppuccin theme",
|
||||
"description": "Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Frappe",
|
||||
@@ -1627,6 +1990,28 @@
|
||||
"Base0F"
|
||||
]
|
||||
},
|
||||
"floating_border": {
|
||||
"description": "Border colour when the window is floating (default: Base09)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Base00",
|
||||
"Base01",
|
||||
"Base02",
|
||||
"Base03",
|
||||
"Base04",
|
||||
"Base05",
|
||||
"Base06",
|
||||
"Base07",
|
||||
"Base08",
|
||||
"Base09",
|
||||
"Base0A",
|
||||
"Base0B",
|
||||
"Base0C",
|
||||
"Base0D",
|
||||
"Base0E",
|
||||
"Base0F"
|
||||
]
|
||||
},
|
||||
"monocle_border": {
|
||||
"description": "Border colour when the container is in monocle mode (default: Base0F)",
|
||||
"type": "string",
|
||||
@@ -1650,7 +2035,7 @@
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the Base16 theme",
|
||||
"description": "Name of the Base16 theme (theme previews: https://tinted-theming.github.io/base16-gallery)",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"3024",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
<Product Id='*' Name='komorebi' UpgradeCode='F8B967B5-7E7B-4E3A-895B-B789EC898B54' Manufacturer='LGUG2Z' Language='1033' Codepage='1252' Version='$(var.Version)'>
|
||||
|
||||
<Package Id='*' Keywords='Installer' Description='A tiling window manager for Windows' Manufacturer='LGUG2Z' InstallerVersion='450' Languages='1033' Compressed='yes' InstallScope='perMachine' SummaryCodepage='1252' />
|
||||
<Package Id='*' Keywords='Installer' Description='A tiling window manager for Windows' Manufacturer='LGUG2Z' InstallerVersion='500' Languages='1033' Compressed='yes' InstallScope='perMachine' SummaryCodepage='1252' />
|
||||
|
||||
<MajorUpgrade Schedule='afterInstallInitialize' DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.' />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user