Compare commits

..

2 Commits

Author SHA1 Message Date
LGUG2Z
8ceed09c20 fix(wm): dynamically reserve monitor ring space
This commit makes a small change to dynamically keep reserving space in
the VecDeque that backs Ring<Monitor> until an index preference can be
contained within the current length.
2024-05-23 16:58:54 -07:00
LGUG2Z
1712042dda chore(dev): begin v0.1.27-dev 2024-05-23 16:47:26 -07:00
149 changed files with 4114 additions and 15556 deletions

52
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,52 @@
---
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

View File

@@ -1,56 +0,0 @@
name: Bug report
description: File a bug report
labels: [bug]
title: "[BUG]: "
body:
- type: textarea
validations:
required: true
attributes:
label: Summary
description: >
Please provide a short summary of the bug, along with any information
you feel is relevant to replicating the bug.
You may include screenshots and videos in this section.
- type: textarea
validations:
required: true
attributes:
label: Version Information
description: >
Please provide information about the versions of Windows and komorebi
running on your machine.
Do not submit a bug if you are not using an official version of Windows
such as AtlasOS; only official versions of Windows are supported.
```
systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version"
```
```
komorebic --version
```
- type: textarea
validations:
required: true
attributes:
label: Komorebi Configuration
description: >
Please provide your configuration file (komorebi.json or komorebi.bar.json)
render: json
- type: textarea
validations:
required: true
attributes:
label: Hotkey Configuration
description: >
Please provide your whkdrc or komorebi.ahk hotkey configuration file
- type: textarea
validations:
required: true
attributes:
label: Output of komorebic check
description: >
Please provide the output of `komorebic check`

View File

@@ -1,8 +0,0 @@
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

View File

@@ -0,0 +1,20 @@
---
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.

View File

@@ -1,37 +0,0 @@
name: Feature request
description: Suggest a new feature
labels: [enhancement]
title: "[FEAT]: "
body:
- type: dropdown
id: Sponsors
attributes:
label: Sponsorship Information
description: >
Feature requests are considered from individuals who are $5+ monthly sponsors to the project.
Please specify the platform you use to sponsor the project.
options:
- GitHub Sponsors
- Ko-fi
- Discord
- YouTube
default: 0
validations:
required: true
- type: textarea
validations:
required: true
attributes:
label: Suggestion
description: >
Please share your suggestion here. Be sure to include all necessary context.
If you sponsor on a platform where you use a different username, please specify the username here.
- type: textarea
validations:
required: true
attributes:
label: Alternatives Considered
description: >
Please share share alternatives you have considered here.

View File

@@ -14,189 +14,125 @@ on:
tags:
- v*
schedule:
- cron: "30 0 * * 0" # Every day at 00:30 UTC
- cron: "30 0 * * 1" # Every Monday at half past midnight UTC
jobs:
build:
name: Build
runs-on: windows-latest
env:
RUSTFLAGS: -Ctarget-feature=+crt-static
strategy:
fail-fast: true
fail-fast: false
matrix:
platform:
- os-name: Windows-x86_64
runs-on: windows-latest
target: x86_64-pc-windows-msvc
- os-name: Windows-aarch64
runs-on: windows-latest
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.platform.runs-on }}
permissions: write-all
env:
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
GH_TOKEN: ${{ github.token }}
target:
- x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: rustup toolchain install stable --profile minimal
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
- uses: Swatinem/rust-cache@v2
- 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
with:
cache-on-failure: "true"
cache-all-crates: "true"
key: ${{ matrix.platform.target }}
- run: cargo +nightly fmt --check
- run: cargo clippy
- uses: houseabsolute/actions-rust-cross@v0
with:
command: "build"
target: ${{ matrix.platform.target }}
args: "--locked --release"
- run: |
cargo install cargo-wix
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v4
with:
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
path: |
target/${{ matrix.platform.target }}/release/*.exe
target/${{ matrix.platform.target }}/release/*.pdb
~/.cargo/registry
~/.cargo/git
~/.cargo/bin
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Get rustc commit hash
id: cargo-target-cache
run: |
echo "::set-output name=rust_hash::$(rustc -Vv | grep commit-hash | awk '{print $2}')"
shell: bash
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ github.base_ref }}-${{ github.head_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ github.base_ref }}-${{ matrix.target }}-cargo-target-dir-${{ steps.cargo-target-cache.outputs.rust_hash }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install Rustup using win.rustup.rs
run: |
# Disable the download progress bar which can cause perf issues
$ProgressPreference = "SilentlyContinue"
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --profile=minimal
shell: powershell
- name: Ensure stable toolchain is up to date
run: rustup update stable
shell: bash
- name: Install the target
run: |
rustup target install ${{ matrix.target }}
- name: Run Cargo checks
run: |
cargo fmt --check
cargo check
cargo clippy
- name: Run a full build
run: |
cargo build --locked --release --target ${{ matrix.target }}
- name: Create MSI installer
run: |
cargo install cargo-wix
cargo wix -p komorebi --nocapture -I .\wix\main.wxs --target x86_64-pc-windows-msvc
- name: Upload the built artifacts
uses: actions/upload-artifact@v4
with:
name: komorebi-${{ matrix.target }}
path: |
target/${{ matrix.target }}/release/komorebi.exe
target/${{ matrix.target }}/release/komorebic.exe
target/${{ matrix.target }}/release/komorebic-no-console.exe
target/${{ matrix.target }}/release/komorebi-gui.exe
target/${{ matrix.target }}/release/komorebi.pdb
target/${{ matrix.target }}/release/komorebic.pdb
target/${{ matrix.target }}/release/komorebi-gui.pdb
target/wix/komorebi-*.msi
retention-days: 14
nightly:
needs: build
runs-on: windows-latest
permissions: write-all
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
retention-days: 7
- name: Check GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
fetch-depth: 0
- shell: bash
run: echo "VERSION=nightly" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
version: latest
args: build --skip=validate --clean
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
cache-all-crates: "true"
- shell: bash
# Release
- name: Generate changelog
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
run: |
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly || true
git tag nightly
kokai release --no-emoji --add-links github:commits,issues --ref nightly >"CHANGELOG.md"
- shell: bash
run: |
gh release delete nightly --yes || true
git push origin :nightly || true
gh release create nightly \
--target $GITHUB_SHA \
--prerelease \
--title "komorebi nightly (${GITHUB_SHA})" \
--notes-file CHANGELOG.md
komorebi-nightly-x86_64-pc-windows-msvc.zip \
komorebi-nightly-x86_64.msi \
komorebi-nightly-aarch64-pc-windows-msvc.zip \
komorebi-nightly-aarch64.msi \
checksums.txt
release-dry-run:
needs: build
runs-on: windows-latest
permissions: write-all
if: ${{ github.ref == 'refs/heads/master' }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
cache-all-crates: "true"
- shell: bash
run: |
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly || true
kokai release --no-emoji --add-links github:commits,issues --ref "${{ github.ref_name }}" >"CHANGELOG.md"
- uses: softprops/action-gh-release@v2
with:
draft: true
body_path: "CHANGELOG.md"
files: |
checksums.txt
*.zip
*.msi
release:
needs: build
runs-on: windows-latest
permissions: write-all
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip" >checksums.txt
Compress-Archive -Force ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/aarch64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip
Copy-Item ./komorebi-aarch64-pc-windows-msvc-${{ github.sha }}/wix/*aarch64.msi -Destination ./komorebi-$Env:VERSION-aarch64.msi
echo "$((Get-FileHash komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-$Env:VERSION-aarch64-pc-windows-msvc.zip" >>checksums.txt
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
cache-all-crates: "true"
- shell: bash
run: |
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly || true
kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md"
- uses: softprops/action-gh-release@v2
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
if: startsWith(github.ref, 'refs/tags/v')
with:
body_path: "CHANGELOG.md"
files: |
checksums.txt
*.zip
*.msi
version: latest
args: release --skip=validate --clean --release-notes=CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCOOP_TOKEN: ${{ secrets.SCOOP_TOKEN }}
- name: Add MSI to release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
files: "target/wix/komorebi-*.msi"
winget:
name: Publish to WinGet
runs-on: ubuntu-latest
needs: release
needs: build
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:

4
.gitignore vendored
View File

@@ -3,5 +3,5 @@
/target
CHANGELOG.md
dummy.go
komorebic/applications.yaml
/.vs
komorebi.ahk
komorebic/applications.yaml

59
.goreleaser.yml Normal file
View File

@@ -0,0 +1,59 @@
# Adapted from https://jondot.medium.com/shipping-rust-binaries-with-goreleaser-d5aa42a46be0
project_name: komorebi
before:
hooks:
- powershell.exe -Command "New-Item -Path . -Name dummy.go -ItemType file -Force"
- powershell.exe -Command "Add-Content -Path .\dummy.go -Value 'package main'"
- powershell.exe -Command "Add-Content -Path .\dummy.go -Value 'func main() {}'"
builds:
- id: komorebi
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi.exe" ".\dist\komorebi_windows_amd64_v1\komorebi.exe"
- id: komorebic
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebic
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64_v1\komorebic.exe"
- id: komorebic-no-console
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebic-no-console
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic-no-console.exe" ".\dist\komorebic-no-console_windows_amd64_v1\komorebic-no-console.exe"
- id: komorebi-gui
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi-gui
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-gui.exe" ".\dist\komorebi-gui_windows_amd64_v1\komorebi-gui.exe"
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
format: zip
files:
- LICENSE.md
- CHANGELOG.md
checksum:
name_template: checksums.txt
changelog:
sort: asc

3347
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,46 +2,28 @@
resolver = "2"
members = [
"derive-ahk",
"komorebi",
"komorebi-client",
"komorebi-core",
"komorebi-gui",
"komorebic",
"komorebic-no-console",
"komorebi-bar",
"komorebi-themes"
]
[workspace.dependencies]
clap = { version = "4", features = ["derive", "wrap_help"] }
chrono = "0.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
color-eyre = "0.6"
eframe = "0.29"
egui_extras = "0.29"
dirs = "5"
windows-interface = { version = "0.53" }
windows-implement = { version = "0.53" }
dunce = "1"
hotwatch = "0.5"
schemars = "0.8"
lazy_static = "1"
dirs = "5"
color-eyre = "0.6"
serde_json = { package = "serde_json_lenient", version = "0.1" }
sysinfo = "0.30"
serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" }
serde_yaml = "0.9"
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
paste = "1"
sysinfo = "0.31"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
windows-implement = { version = "0.58" }
windows-interface = { version = "0.58" }
windows-core = { version = "0.58" }
shadow-rs = "0.35"
which = "6"
[workspace.dependencies.windows]
version = "0.58"
version = "0.54"
features = [
"implement",
"Win32_System_Com",
@@ -58,8 +40,5 @@ features = [
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming",
"Media",
"Media_Control"
"Win32_System_SystemServices"
]

View File

@@ -1,6 +1,6 @@
# Komorebi License
# PolyForm Strict License 1.0.0
Version 1.0.0
<https://polyformproject.org/licenses/strict/1.0.0>
## Acceptance
@@ -13,14 +13,8 @@ your licenses.
The licensor grants you a copyright license for the software
to do everything you might do with the software that would
otherwise infringe the licensor's copyright in it for any
permitted purpose. However, you may only make changes according
to the [Changes License](#changes-license), and you may not
distribute the software or new works based on the software.
## Changes License
The licensor grants you an additional copyright license to
make changes for any permitted purpose.
permitted purpose, other than distributing the software or
making changes or new works based on the software.
## Patent License
@@ -28,6 +22,10 @@ The licensor grants you a patent license for the software that
covers patent claims the licensor can license, or becomes able
to license, that you would infringe by using the software.
## Noncommercial Purposes
Any noncommercial purpose is a permitted purpose.
## Personal Uses
Personal use for research, experiment, and testing for
@@ -36,6 +34,15 @@ entertainment, hobby projects, amateur pursuits, or religious
observance, without any anticipated commercial application,
is use for a permitted purpose.
## Noncommercial Organizations
Use by any charitable organization, educational institution,
public research organization, public safety or health
organization, environmental protection organization,
or government institution is use for a permitted purpose
regardless of the source of funding or obligations resulting
from the funding.
## Fair Use
You may have "fair use" rights for the software under the

View File

@@ -1,8 +0,0 @@
# Privacy Policy for Komorebi
No data about your device(s) or _komorebi_ usage leave your device.
## Data Maintained by Komorebi
_komorebi_ writes log files to and keeps a list of temporary window handles (HWNDs) currently managed by the process in
the `$Env:LOCALAPPDATA\komorebi\` directory. This directory is owned by the user running the process.

View File

@@ -60,7 +60,7 @@ channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
_komorebi_ development videos. If you would like to be notified of upcoming
videos please subscribe and turn on notifications.
_komorebi_ is a free and source-available project, and one that encourages you to
_komorebi_ is a free and open-source project, and one that encourages you to
make charitable donations if you find the software to be useful and have the
financial means.
@@ -82,7 +82,7 @@ A [detailed installation and quickstart
guide](https://lgug2z.github.io/komorebi/installation.html) is available which shows how to get started
using `scoop`, `winget` or building from source.
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/MMZUAtHbTYY/hqdefault.jpg)](https://www.youtube.com/watch?v=MMZUAtHbTYY)
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/H9-_c1egQ4g/hqdefault.jpg)](https://www.youtube.com/watch?v=H9-_c1egQ4g)
# Comparison With Fancy Zones
@@ -99,13 +99,6 @@ video will answer the majority of your questions.
# Demonstrations
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
unfocused window transparency and animations enabled, using a custom status bar integrated using
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
11 with a terminal emulator, a web browser and a code editor. The original
video can be viewed
@@ -191,8 +184,7 @@ required.
## License
`komorebi` is licensed under the [Komorebi 1.0.0 license](./LICENSE.md), which
is a fork of the [PolyForm Strict 1.0.0
`komorebi` is licensed under 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
@@ -280,7 +272,7 @@ If the named pipe exists, `komorebi` will start pushing JSON data of successfull
You may then filter on the `type` key to listen to the events that you are interested in. For a full list of possible
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
in `komorebi::core`.
in `komorebi-core`.
Below is an example of how you can subscribe to and filter on events using a named pipe in `nodejs`.
@@ -359,7 +351,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.25"}
use anyhow::Result;
use komorebi_client::Notification;
@@ -415,7 +407,7 @@ A TCP listener can optionally be exposed on a port of your choosing with the `--
provided to `komorebi` or `komorebic start`, no TCP listener will be created.
Once created, your client may send
any [SocketMessage](https://github.com/LGUG2Z/komorebi/blob/master/komorebi/src/core/mod.rs#L37) to `komorebi` in the
any [SocketMessage](https://github.com/LGUG2Z/komorebi/blob/master/komorebi-core/src/lib.rs#L37) to `komorebi` in the
same way that `komorebic` would.
This can be used if you would like to create your own alternative to `komorebic` which incorporates scripting and

14
derive-ahk/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "derive-ahk"
version = "0.1.1"
edition = "2021"
[lib]
proc-macro = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "1.0"
syn = "1.0"
quote = "1.0"

225
derive-ahk/src/lib.rs Normal file
View File

@@ -0,0 +1,225 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![no_implicit_prelude]
use ::std::clone::Clone;
use ::std::convert::From;
use ::std::convert::Into;
use ::std::format;
use ::std::iter::Extend;
use ::std::iter::Iterator;
use ::std::matches;
use ::std::option::Option::Some;
use ::std::string::String;
use ::std::string::ToString;
use ::std::unreachable;
use ::std::vec::Vec;
use ::quote::quote;
use ::syn::parse_macro_input;
use ::syn::Data;
use ::syn::DataEnum;
use ::syn::DeriveInput;
use ::syn::Fields;
use ::syn::FieldsNamed;
use ::syn::FieldsUnnamed;
use ::syn::Meta;
use ::syn::NestedMeta;
#[allow(clippy::too_many_lines)]
#[proc_macro_derive(AhkFunction)]
pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
match input.data {
Data::Struct(s) => match s.fields {
Fields::Named(FieldsNamed { named, .. }) => {
let argument_idents = named
.iter()
// Filter out the flags
.filter(|&f| {
let mut include = true;
for attribute in &f.attrs {
if let ::std::result::Result::Ok(Meta::List(list)) =
attribute.parse_meta()
{
for nested in list.nested {
if let NestedMeta::Meta(Meta::Path(path)) = nested {
if path.is_ident("long") {
include = false;
}
}
}
}
}
include
})
.map(|f| &f.ident);
let argument_idents_clone = argument_idents.clone();
let called_arguments = quote! {#(%#argument_idents_clone%) *}
.to_string()
.replace(" %", "%")
.replace("% ", "%")
.replace("%%", "% %");
let flag_idents = named
.iter()
// Filter only the flags
.filter(|f| {
let mut include = false;
for attribute in &f.attrs {
if let ::std::result::Result::Ok(Meta::List(list)) =
attribute.parse_meta()
{
for nested in list.nested {
if let NestedMeta::Meta(Meta::Path(path)) = nested {
// Identify them using the --long flag name
if path.is_ident("long") {
include = true;
}
}
}
}
}
include
})
.map(|f| &f.ident);
let has_flags = flag_idents.clone().count() != 0;
if has_flags {
let flag_idents_concat = flag_idents.clone();
let argument_idents_concat = argument_idents.clone();
// Concat the args and flag args if there are flags
let all_arguments =
quote! {#(#argument_idents_concat,) * #(#flag_idents_concat), *}
.to_string();
let flag_idents_clone = flag_idents.clone();
let flags = quote! {#(--#flag_idents_clone) *}
.to_string()
.replace("- - ", "--")
.replace('_', "-");
let called_flag_arguments = quote! {#(%#flag_idents%) *}
.to_string()
.replace(" %", "%")
.replace("% ", "%")
.replace("%%", "% %");
let flags_split: Vec<_> = flags.split(' ').collect();
let flag_args_split: Vec<_> = called_flag_arguments.split(' ').collect();
let mut consolidated_flags: Vec<String> = Vec::new();
for (idx, flag) in flags_split.iter().enumerate() {
consolidated_flags.push(format!("{} {}", flag, flag_args_split[idx]));
}
let all_flags = consolidated_flags.join(" ");
quote! {
impl AhkFunction for #name {
fn generate_ahk_function() -> String {
::std::format!(r#"
{}({}) {{
RunWait, komorebic.exe {} {} {}, , Hide
}}"#,
::std::stringify!(#name),
#all_arguments,
::std::stringify!(#name).to_kebab_case(),
#called_arguments,
#all_flags,
)
}
}
}
} else {
let arguments = quote! {#(#argument_idents), *}.to_string();
quote! {
impl AhkFunction for #name {
fn generate_ahk_function() -> String {
::std::format!(r#"
{}({}) {{
RunWait, komorebic.exe {} {}, , Hide
}}"#,
::std::stringify!(#name),
#arguments,
::std::stringify!(#name).to_kebab_case(),
#called_arguments
)
}
}
}
}
}
_ => unreachable!("only to be used on structs with named fields"),
},
_ => unreachable!("only to be used on structs"),
}
.into()
}
#[proc_macro_derive(AhkLibrary)]
pub fn ahk_library(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
match input.data {
Data::Enum(DataEnum { variants, .. }) => {
let enums = variants.iter().filter(|&v| {
matches!(v.fields, Fields::Unit) || matches!(v.fields, Fields::Unnamed(..))
});
let mut stream = ::proc_macro2::TokenStream::new();
for variant in enums.clone() {
match &variant.fields {
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
for field in unnamed {
stream.extend(quote! {
v.push(#field::generate_ahk_function());
});
}
}
Fields::Unit => {
let name = &variant.ident;
stream.extend(quote! {
v.push(::std::format!(r#"
{}() {{
RunWait, komorebic.exe {}, , Hide
}}"#,
::std::stringify!(#name),
::std::stringify!(#name).to_kebab_case()
));
});
}
Fields::Named(_) => {
unreachable!("only to be used with unnamed and unit fields");
}
}
}
quote! {
impl #name {
fn generate_ahk_library() -> String {
let mut v: Vec<String> = vec![String::from("; Generated by komorebic.exe")];
#stream
v.join("\n")
}
}
}
}
_ => unreachable!("only to be used on enums"),
}
.into()
}

12
docs/cli/ahk-library.md Normal file
View File

@@ -0,0 +1,12 @@
# ahk-library
```
Generate a library of AutoHotKey helper functions
Usage: komorebic.exe ahk-library
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# animation-duration
```
Set the duration for movement animations in ms
Usage: komorebic.exe animation-duration <DURATION>
Arguments:
<DURATION>
Desired animation durations in ms
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# animation-fps
```
Set the frames per second for movement animations
Usage: komorebic.exe animation-fps <FPS>
Arguments:
<FPS>
Desired animation frames per second
Options:
-h, --help
Print help
```

View File

@@ -1,20 +0,0 @@
# animation-style
```
Set the ease function for movement animations
Usage: komorebic.exe animation-style [OPTIONS]
Options:
-s, --style <STYLE>
Desired ease function for animation
[default: linear]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart,
ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ,
ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# animation
```
Enable or disable movement animations
Usage: komorebic.exe animation <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
Options:
-h, --help
Print help
```

View File

@@ -1,12 +0,0 @@
# bar-configuration
```
Show the path to komorebi.bar.json
Usage: komorebic.exe bar-configuration
Options:
-h, --help
Print help
```

View File

@@ -18,7 +18,7 @@ Arguments:
Options:
-w, --window-kind <WINDOW_KIND>
[default: single]
[possible values: single, stack, monocle, unfocused, floating]
[possible values: single, stack, monocle, unfocused]
-h, --help
Print help

View File

@@ -1,20 +0,0 @@
# border-implementation
```
Set the border implementation
Usage: komorebic.exe border-implementation <STYLE>
Arguments:
<STYLE>
Desired border implementation
Possible values:
- komorebi: Use the adjustable komorebi border implementation
- windows: Use the thin Windows accent border implementation
Options:
-h, --help
Print help (see a summary with '-h')
```

View File

@@ -1,21 +0,0 @@
# border-style
```
Set the border style
Usage: komorebic.exe border-style <STYLE>
Arguments:
<STYLE>
Desired border style
Possible values:
- system: Use the system border style
- rounded: Use the Windows 11-style rounded borders
- square: Use the Windows 10-style square borders
Options:
-h, --help
Print help (see a summary with '-h')
```

View File

@@ -1,12 +0,0 @@
# clear-all-workspace-rules
```
Remove all application association rules for all workspaces
Usage: komorebic.exe clear-all-workspace-rules
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# clear-named-workspace-rules
```
Remove all application association rules for a named workspace
Usage: komorebic.exe clear-named-workspace-rules <WORKSPACE>
Arguments:
<WORKSPACE>
Name of a workspace
Options:
-h, --help
Print help
```

View File

@@ -1,19 +0,0 @@
# clear-workspace-rules
```
Remove all application association rules for a workspace by monitor and workspace index
Usage: komorebic.exe clear-workspace-rules <MONITOR> <WORKSPACE>
Arguments:
<MONITOR>
Monitor index (zero-indexed)
<WORKSPACE>
Workspace index on the specified monitor (zero-indexed)
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# complete-configuration
```
For legacy komorebi.ahk or komorebi.ps1 configurations, signal that the final configuration option has been sent
Signal that the final configuration option has been sent
Usage: komorebic.exe complete-configuration

View File

@@ -1,16 +0,0 @@
# convert-app-specific-configuration
```
Convert a v1 ASC YAML file to a v2 ASC JSON file
Usage: komorebic.exe convert-app-specific-configuration <PATH>
Arguments:
<PATH>
YAML file from which the application-specific configurations should be loaded
Options:
-h, --help
Print help
```

View File

@@ -9,15 +9,15 @@ 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

View File

@@ -1,9 +1,9 @@
# ignore-rule
# float-rule
```
Add a rule to ignore the specified application
Add a rule to always float the specified application
Usage: komorebic.exe ignore-rule <IDENTIFIER> <ID>
Usage: komorebic.exe float-rule <IDENTIFIER> <ID>
Arguments:
<IDENTIFIER>

View File

@@ -0,0 +1,23 @@
# focus-follows-mouse
```
Enable or disable focus follows mouse for the operating system
Usage: komorebic.exe focus-follows-mouse [OPTIONS] <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
Options:
-i, --implementation <IMPLEMENTATION>
[default: windows]
Possible values:
- komorebi: A custom FFM implementation (slightly more CPU-intensive)
- windows: The native (legacy) Windows FFM implementation
-h, --help
Print help (see a summary with '-h')
```

View File

@@ -1,16 +0,0 @@
# focus-stack-window
```
Focus the specified window index in the focused stack
Usage: komorebic.exe focus-stack-window <TARGET>
Arguments:
<TARGET>
Target index (zero-indexed)
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# format-app-specific-configuration
```
Format a YAML file for use with the 'app-specific-configuration' command
Format a YAML file for use with the 'ahk-app-specific-configuration' command
Usage: komorebic.exe format-app-specific-configuration <PATH>

View File

@@ -0,0 +1,16 @@
# load-custom-layout
```
Load a custom layout from file for the focused workspace
Usage: komorebic.exe load-custom-layout <PATH>
Arguments:
<PATH>
JSON or YAML file from which the custom layout definition should be loaded
Options:
-h, --help
Print help
```

View File

@@ -1,12 +0,0 @@
# monitor-information
```
Show information about connected monitors
Usage: komorebic.exe monitor-information
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,22 @@
# named-workspace-custom-layout-rule
```
Add a dynamic custom layout for the specified workspace
Usage: komorebic.exe named-workspace-custom-layout-rule <WORKSPACE> <AT_CONTAINER_COUNT> <PATH>
Arguments:
<WORKSPACE>
Target workspace name
<AT_CONTAINER_COUNT>
The number of window containers on-screen required to trigger this layout rule
<PATH>
JSON or YAML file from which the custom layout definition should be loaded
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,19 @@
# named-workspace-custom-layout
```
Set a custom layout for the specified workspace
Usage: komorebic.exe named-workspace-custom-layout <WORKSPACE> <PATH>
Arguments:
<WORKSPACE>
Target workspace name
<PATH>
JSON or YAML file from which the custom layout definition should be loaded
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# reload-configuration
```
Reload legacy komorebi.ahk or komorebi.ps1 configurations (if they exist)
Reload ~/komorebi.ahk (if it exists)
Usage: komorebic.exe reload-configuration

View File

@@ -1,16 +0,0 @@
# replace-configuration
```
Replace the configuration of a running instance of komorebi from a static configuration file
Usage: komorebic.exe replace-configuration <PATH>
Arguments:
<PATH>
Static configuration JSON file from which the configuration should be loaded
Options:
-h, --help
Print help
```

View File

@@ -1,12 +0,0 @@
# stack-all
```
Stack all windows on the focused workspace
Usage: komorebic.exe stack-all
Options:
-h, --help
Print help
```

View File

@@ -6,6 +6,9 @@ Start komorebi.exe as a background process
Usage: komorebic.exe start [OPTIONS]
Options:
-f, --ffm
Allow the use of komorebi's custom focus-follows-mouse implementation
-c, --config <CONFIG>
Path to a static configuration JSON file
@@ -21,9 +24,6 @@ Options:
--ahk
Start autohotkey configuration file
--bar
Start komorebi-bar in a background process
-h, --help
Print help

View File

@@ -9,12 +9,6 @@ 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
-h, --help
Print help

View File

@@ -1,12 +0,0 @@
# toggle-float-override
```
Enable or disable float override, which makes it so every new window opens in floating mode
Usage: komorebic.exe toggle-float-override
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,19 @@
# toggle-focus-follows-mouse
```
Toggle focus follows mouse for the operating system
Usage: komorebic.exe toggle-focus-follows-mouse [OPTIONS]
Options:
-i, --implementation <IMPLEMENTATION>
[default: windows]
Possible values:
- komorebi: A custom FFM implementation (slightly more CPU-intensive)
- windows: The native (legacy) Windows FFM implementation
-h, --help
Print help (see a summary with '-h')
```

View File

@@ -1,12 +0,0 @@
# toggle-transparency
```
Toggle transparency for unfocused windows
Usage: komorebic.exe toggle-transparency
Options:
-h, --help
Print help
```

View File

@@ -1,13 +0,0 @@
# toggle-workspace-float-override
```
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace
previously it takes the opposite of the global value
Usage: komorebic.exe toggle-workspace-float-override
Options:
-h, --help
Print help
```

View File

@@ -1,13 +0,0 @@
# toggle-workspace-window-container-behaviour
```
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the
global value
Usage: komorebic.exe toggle-workspace-window-container-behaviour
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# transparency-alpha
```
Set the alpha value for unfocused window transparency
Usage: komorebic.exe transparency-alpha <ALPHA>
Arguments:
<ALPHA>
Alpha
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# transparency
```
Enable or disable transparency for unfocused windows
Usage: komorebic.exe transparency <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
Options:
-h, --help
Print help
```

View File

@@ -1,12 +0,0 @@
# unstack-all
```
Unstack all windows in the focused container
Usage: komorebic.exe unstack-all
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# watch-configuration
```
Enable or disable watching of legacy komorebi.ahk or komorebi.ps1 configurations (if they exist)
Enable or disable watching of ~/komorebi.ahk (if it exists)
Usage: komorebic.exe watch-configuration <BOOLEAN_STATE>

View File

@@ -10,7 +10,7 @@ Arguments:
Possible values:
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
Options:
-h, --help

View File

@@ -0,0 +1,25 @@
# workspace-custom-layout-rule
```
Add a dynamic custom layout for the specified workspace
Usage: komorebic.exe workspace-custom-layout-rule <MONITOR> <WORKSPACE> <AT_CONTAINER_COUNT> <PATH>
Arguments:
<MONITOR>
Monitor index (zero-indexed)
<WORKSPACE>
Workspace index on the specified monitor (zero-indexed)
<AT_CONTAINER_COUNT>
The number of window containers on-screen required to trigger this layout rule
<PATH>
JSON or YAML file from which the custom layout definition should be loaded
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,22 @@
# workspace-custom-layout
```
Set a custom layout for the specified workspace
Usage: komorebic.exe workspace-custom-layout <MONITOR> <WORKSPACE> <PATH>
Arguments:
<MONITOR>
Monitor index (zero-indexed)
<WORKSPACE>
Workspace index on the specified monitor (zero-indexed)
<PATH>
JSON or YAML file from which the custom layout definition should be loaded
Options:
-h, --help
Print help
```

View File

@@ -1,25 +0,0 @@
# Animations
If you would like to add window movement animations, ensure the following options are
defined in the `komorebi.json` configuration file.
```json
{
"animation": {
"enabled": true
}
}
```
Window movement animations only apply to actions taking place within the same monitor
workspace.
You can optionally set a custom duration in ms with `animation.duration` (default: `250`),
a custom style with `animation.style` (default: `Linear`), and a custom FPS value with
`animation.fps` (default: `60`).
It is important to note that higher `fps` and a longer `duration` settings will result
in increased CPU usage.
This feature is not considered stable, and you may encounter visual artifacts
from time to time.

View File

@@ -1,4 +1,4 @@
# AutoHotkey
# AutoHotKey
If you would like to use Autohotkey, please make sure you have AutoHotKey v2
installed.
@@ -10,8 +10,8 @@ able to craft their own configuration files.
If you would like to try out AHK, here is a simple sample configuration which
largely matches the `whkdrc` sample configuration.
```autohotkey
{% include "./komorebi.ahk.txt" %}
```
{% include "../komorebi.ahk" %}
```
By default, the `komorebi.ahk` file should be located in the `$Env:USERPROFILE`
@@ -19,4 +19,4 @@ directory, however, if `$Env:KOMOREBI_CONFIG_HOME` is set, it should be located
there.
Once the file is in place, you can stop komorebi and whkd by running `komorebic stop --whkd`,
and then start komorebi with Autohotkey by running `komorebic start --ahk`.
and then start komorebi with Autohotkey by running `komorebic start --ahk`.

View File

@@ -1,29 +0,0 @@
# Autostart
If you would like to autostart `komorebi`, you can use the `komorebic enable-autostart` command to generate a shortcut
in the `shell:startup` folder.
```
Generates the komorebi.lnk shortcut in shell:startup to autostart komorebi
Usage: komorebic.exe enable-autostart [OPTIONS]
Options:
-c, --config <CONFIG>
Path to a static configuration JSON file
-f, --ffm
Enable komorebi's custom focus-follows-mouse implementation
--whkd
Enable autostart of whkd
--ahk
Enable autostart of ahk
--bar
Enable autostart of komorebi-bar
-h, --help
Print help
```

View File

@@ -0,0 +1,70 @@
# Custom Layouts
Particularly for users of ultrawide monitors, traditional tiling layouts may
not seem like the most efficient use of screen space. If you feel this is the
case with any of the default layouts, you are also welcome to create your own
custom layouts and save them as JSON or YAML.
If you're not comfortable writing the layouts directly in JSON or YAML, you can
use the [komorebi Custom Layout
Generator](https://lgug2z.github.io/komorebi-custom-layout-generator/) to
interactively define a custom layout, and then copy the generated JSON content.
Custom layouts can be loaded on the current workspace or configured for a
specific workspace in the `komorebi.json` configuration file.
```json
{
"monitors": [
{
"workspaces": [
{
"name": "personal",
"custom_layout": "C:/Users/LGUG2Z/my-custom-layout.json"
},
]
}
]
}
```
The fundamental building block of a custom _komorebi_ layout is the Column.
Columns come in three variants:
- **Primary**: This is where your primary focus will be on the screen most of
the time. There must be exactly one Primary Column in any custom layout.
Optionally, you can specify the percentage of the screen width that you want
the Primary Column to occupy.
- **Secondary**: This is an optional column that can either be full height of
split horizontally into a fixed number of maximum rows. There can be any
number of Secondary Columns in a custom layout.
- **Tertiary**: This is the final column where any remaining windows will be
split horizontally into rows as they get added.
If there is only one window on the screen when a custom layout is selected,
that window will take up the full work area of the screen.
If the number of windows is equal to or less than the total number of columns
defined in a custom layout, the windows will be arranged in an equal-width
columns.
When the number of windows is greater than the number of columns defined in the
custom layout, the windows will begin to be arranged according to the
constraints set on the Primary and Secondary columns of the layout.
Here is an example custom layout that can be used as a starting point for your
own:
```yaml
- column: Secondary
configuration: !Horizontal 2 # max number of rows
- column: Primary
configuration: !WidthPercentage 50 # percentage of screen
- column: Tertiary
configuration: Horizontal
```
<!-- TODO: Record a new video -->
[![Watch the tutorial video](https://img.youtube.com/vi/SgmBHKEOcQ4/hqdefault.jpg)](https://www.youtube.com/watch?v=SgmBHKEOcQ4)

View File

@@ -1,4 +1,4 @@
# Dynamic Layout Switching
# Dynamically Layout Switching
With `komorebi` it is possible to define rules to automatically change the
layout on a specified workspace when a threshold of window containers is met.

View File

@@ -1,16 +0,0 @@
# Floating Windows
Sometimes you will want a specific application to be managed as a floating window.
You can add rules to enforce this behaviour in the `komorebi.json` configuration file.
```json
{
"floating_applications": [
{
"kind": "Title",
"id": "Media Player",
"matching_strategy": "Equals"
}
]
}
```

View File

@@ -0,0 +1,34 @@
# Focus Follows Mouse
`komorebi` supports two focus-follows-mouse implementations; the native Windows
Xmouse implementation, which treats the desktop, the task bar, and the system
tray as windows and switches focus to them eagerly, and a custom `komorebi`
implementation, which only considers windows managed by `komorebi` as valid
targets to switch focus to when moving the mouse.
To enable the `komorebi` implementation you must start the process with the
`--ffm` flag to explicitly enable the feature. This is because the mouse
tracking required for this feature significantly increases the CPU usage of the
process (on my machine, it jumps from <1% to ~4~), and this CPU increase
persists regardless of whether focus-follows-mouse is enabled or disabled at
any given time via `komorebic`'s configuration commands.
If the `komorebi` process has been started with the `--ffm` flag, you can
enable focus follows mouse behaviour in the `komorebi.json` configuration file.
```json
{
"focus_follows_mouse": "Komorebi"
}
```
When calling any of the `komorebic` commands related to focus-follows-mouse
functionality, the `windows` implementation will be chosen as the default
implementation. You can optionally specify the `komorebi` implementation by
passing it as an argument to the `--implementation` flag:
```powershell
komorebic.exe toggle-focus-follows-mouse --implementation komorebi
```

View File

@@ -5,12 +5,12 @@ applications are [already generated for
you](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
Sometimes you will want a specific application to never be tiled, and instead
be completely ignored. You can add rules to enforce this behaviour in the
float all the time. You can add rules to enforce this behaviour in the
`komorebi.json` configuration file.
```json
{
"ignore_rules": [
"float_rules": [
{
"kind": "Title",
"id": "Media Player",

View File

@@ -1,19 +0,0 @@
# Multiple Bar Instances
If you would like to run multiple instances of `komorebi-bar` to target different monitors, it is possible to do so
by maintaining multiple `komorebi.bar.json` configuration files and specifying their paths in the `bar_configurations`
array in your `komorebi.json` configuration file.
```json
{
"bar_configurations": [
"C:/Users/LGUG2Z/komorebi.bar.monitor1.json",
"C:/Users/LGUG2Z/komorebi.bar.monitor2.json"
]
}
```
You may also use `$Env:USERPROFILE` or `$Env:KOMOREBI_CONFIG_HOME` when specifying the paths.
The main difference between different `komorebi.bar.json` files will be the value of `monitor.index` which is used to
target the monitor for each instance of `komorebi-bar`.

View File

@@ -1,17 +0,0 @@
# Setting a Given Display to a Specific Index
If you would like `komorebi` to remember monitor index positions, you will need to set the `display_index_preferences`
configuration option in the static configuration file.
Display IDs can be found using `komorebic monitor-information`.
Then, in `komorebi.json`, you simply need to specify the preferred index position for each display ID:
```json
{
"display_index_preferences": {
"0": "DEL4310-5&1a6c0954&0&UID209155",
"1": "<another-display_id>"
}
}
```

View File

@@ -16,17 +16,16 @@ the example files have been downloaded. For most new users this will be in the
komorebic quickstart
```
With the example configurations downloaded, you can now start `komorebi`,
`komorebi-bar` and `whkd`.
With the example configurations downloaded, you can now start `komorebi` and `whkd.
```powershell
komorebic start --whkd --bar
komorebic start --whkd
```
## komorebi.json
The example window manager configuration sets some sane defaults and provides
seven preconfigured workspaces on the primary monitor each with a different
five preconfigured workspaces on the primary monitor each with a different
layout.
```json
@@ -177,7 +176,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.
colon. By default, the `whkdrc` file should be located in the `$Env:USERPROFILE/.config/` directory.
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,25 +187,6 @@ 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
@@ -233,24 +213,3 @@ reference.
If you want to use one of those key codes, put them into lower case and remove
the `VK_` prefix. For example, the keycode `VK_OEM_PLUS` becomes `oem_plus` in
the sample configuration above.
## komorebi.bar.json
The example status bar configuration sets some sane defaults and provides
a number of pre-configured widgets on the primary monitor.
```json
{% include "./komorebi.bar.example.json" %}
```
### Themes
Themes can be set in either `komorebi.json` or `komorebi.bar.json`. If set
in `komorebi.json`, the theme will be applied to both komorebi's borders and
stackbars as well as the status bar.
If set in `komorebi.bar.json`, the theme will only be applied to the status bar.
All [Catppuccin palette variants](https://catppuccin.com/)
and [most Base16 palette variants](https://tinted-theming.github.io/base16-gallery/)
are available as themes.

View File

@@ -1,11 +1,11 @@
# Getting started
`komorebi` is a tiling window manager for Windows that is comprised of two
main binaries, `komorebi.exe`, which contains the window manager itself,
`komorebi` is a tiling window manager for Windows that is comprised comprised
of two main binaries, `komorebi.exe`, which contains the window manager itself,
and `komorebic.exe`, which is the main way to send commands to the tiling
window manager.
It is important to note that neither `komorebi.exe` nor `komorebic.exe` handle
It is important to note that neither `komorebi.exe` or `komorebic.exe` handle
key bindings, because `komorebi` is a tiling window manager and not a hotkey
daemon.
@@ -23,15 +23,11 @@ suggest that once you are familiar with the main `komorebic.exe` commands used
to manipulate the window manager, you use
[AutoHotKey](https://www.autohotkey.com/) to handle your key bindings.
`komorebi` also includes `komorebi-bar.exe`, a simple and reliable status bar which
is deeply integrated with the tiling window manager, and can be customized with
various widgets and themes.
## Installation
`komorebi` is available pre-built to install via
[Scoop](https://scoop.sh/#/apps?q=komorebi) and
[WinGet](https://winget.run/pkg/LGUG2Z/komorebi), and you may also build
[WinGet](https://winget.run/pkg/LGUG2Z/komorebi), and you may also built
it from [source](https://github.com/LGUG2Z/komorebi) if you would prefer.
- [Scoop](#scoop)
@@ -41,7 +37,7 @@ it from [source](https://github.com/LGUG2Z/komorebi) if you would prefer.
## Long path support
It is highly recommended that you enable support for long paths in Windows by
It highly recommended that you enable support for long paths in Windows by
running the following command in an Administrator Terminal before installing
`komorebi`.
@@ -49,7 +45,7 @@ running the following command in an Administrator Terminal before installing
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
```
## Disabling unnecessary system animations
## Disabling Unnecessary System Animations
It is highly recommended that you enable the "Turn off all unnecessary animations (when possible)" option in
"Control Panel > Ease of Access > Ease of Access Centre / Make the computer easier to see" for the best performance with
@@ -119,7 +115,6 @@ cargo +stable install --path komorebi --locked
cargo +stable install --path komorebic --locked
cargo +stable install --path komorebic-no-console --locked
cargo +stable install --path komorebi-gui --locked
cargo +stable install --path komorebi-bar --locked
```
If the binaries have been built and added to your `$PATH` correctly, you should
@@ -133,21 +128,3 @@ an offline machine to install.
Once installed, proceed to get the [example configurations](example-configurations.md) (none of the commands for
first-time set up and running komorebi require an internet connection).
## Uninstallation
Before uninstalling, first run `komorebic stop --whkd --bar` to make sure that
the `komorebi`, `komorebi-bar` and `whkd` processes have been stopped.
Then, depending on whether you installed with Scoop or WinGet, run `scoop
uninstall komorebi whkd` or `winget uninstall LGUG2Z.komorebi LGUG2Z.whkd`.
Finally, you can run the following commands in a PowerShell prompt to clean up
files created by the `quickstart` command and any other runtime files:
```powershell
rm $Env:USERPROFILE\komorebi.json
rm $Env:USERPROFILE\applications.yaml
rm $Env:USERPROFILE\.config\whkdrc
rm -r -Force $Env:LOCALAPPDATA\komorebi
```

View File

@@ -1,71 +0,0 @@
#Requires AutoHotkey v2.0.2
#SingleInstance Force
Komorebic(cmd) {
RunWait(format("komorebic.exe {}", cmd), , "Hide")
}
!q::Komorebic("close")
!m::Komorebic("minimize")
; Focus windows
!h::Komorebic("focus left")
!j::Komorebic("focus down")
!k::Komorebic("focus up")
!l::Komorebic("focus right")
!+[::Komorebic("cycle-focus previous")
!+]::Komorebic("cycle-focus next")
; Move windows
!+h::Komorebic("move left")
!+j::Komorebic("move down")
!+k::Komorebic("move up")
!+l::Komorebic("move right")
; Stack windows
!Left::Komorebic("stack left")
!Down::Komorebic("stack down")
!Up::Komorebic("stack up")
!Right::Komorebic("stack right")
!;::Komorebic("unstack")
![::Komorebic("cycle-stack previous")
!]::Komorebic("cycle-stack next")
; Resize
!=::Komorebic("resize-axis horizontal increase")
!-::Komorebic("resize-axis horizontal decrease")
!+=::Komorebic("resize-axis vertical increase")
!+_::Komorebic("resize-axis vertical decrease")
; Manipulate windows
!t::Komorebic("toggle-float")
!f::Komorebic("toggle-monocle")
; Window manager options
!+r::Komorebic("retile")
!p::Komorebic("toggle-pause")
; Layouts
!x::Komorebic("flip-layout horizontal")
!y::Komorebic("flip-layout vertical")
; Workspaces
!1::Komorebic("focus-workspace 0")
!2::Komorebic("focus-workspace 1")
!3::Komorebic("focus-workspace 2")
!4::Komorebic("focus-workspace 3")
!5::Komorebic("focus-workspace 4")
!6::Komorebic("focus-workspace 5")
!7::Komorebic("focus-workspace 6")
!8::Komorebic("focus-workspace 7")
; Move windows across workspaces
!+1::Komorebic("move-to-workspace 0")
!+2::Komorebic("move-to-workspace 1")
!+3::Komorebic("move-to-workspace 2")
!+4::Komorebic("move-to-workspace 3")
!+5::Komorebic("move-to-workspace 4")
!+6::Komorebic("move-to-workspace 5")
!+7::Komorebic("move-to-workspace 6")
!+8::Komorebic("move-to-workspace 7")

View File

@@ -1,76 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.bar.json",
"monitor": {
"index": 0,
"work_area_offset": {
"left": 0,
"top": 40,
"right": 0,
"bottom": 40
}
},
"font_family": "JetBrains Mono",
"theme": {
"palette": "Base16",
"name": "Ashes",
"accent": "Base0D"
},
"left_widgets": [
{
"Komorebi": {
"workspaces": {
"enable": true,
"hide_empty_workspaces": false
},
"layout": {
"enable": true
},
"focused_window": {
"enable": true,
"show_icon": true
}
}
}
],
"right_widgets": [
{
"Media": {
"enable": true
}
},
{
"Storage": {
"enable": true
}
},
{
"Memory": {
"enable": true
}
},
{
"Network": {
"enable": true,
"show_total_data_transmitted": true,
"show_network_activity": true
}
},
{
"Date": {
"enable": true,
"format": "DayDateMonthYear"
}
},
{
"Time": {
"enable": true,
"format": "TwentyFourHour"
}
},
{
"Battery": {
"enable": true
}
}
]
}

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.25/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",
@@ -8,17 +8,20 @@
"border": true,
"border_width": 8,
"border_offset": -1,
"theme": {
"palette": "Base16",
"name": "Ashes",
"unfocused_border": "Base03",
"bar_accent": "Base0D"
"border_colours": {
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399",
"unfocused": "#808080"
},
"stackbar": {
"height": 40,
"mode": "OnStack",
"tabs": {
"width": 300
"width": 300,
"focused_text": "#00a542",
"unfocused_text": "#b3b3b3",
"background": "#141414"
}
},
"monitors": [

56
docs/release/v0-1-22.md Normal file
View File

@@ -0,0 +1,56 @@
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
}
```

View File

@@ -7,24 +7,22 @@ clean:
fmt:
cargo +nightly fmt
cargo +stable clippy
prettier --write .github/ISSUE_TEMPLATE/bug_report.yml
prettier --write .github/ISSUE_TEMPLATE/config.yml
prettier --write .github/ISSUE_TEMPLATE/feature_request.yml
prettier --write README.md
prettier --write .goreleaser.yml
prettier --write .github/dependabot.yml
prettier --write .github/FUNDING.yml
prettier --write .github/workflows/windows.yaml
install-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
install-target target:
cargo +stable install --path {{ target }} --locked
install:
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
just install-target komorebic
just install-target komorebic-no-console
just install-target komorebi
run target:
cargo +stable run --bin {{ target }} --locked
run:
cargo +stable run --bin komorebi --locked
warn $RUST_LOG="warn":
just run
@@ -42,16 +40,9 @@ deadlock $RUST_LOG="trace":
cargo +stable run --bin komorebi --locked --features deadlock_detection
docgen:
cargo run --package komorebic -- docgen
komorebic docgen
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
schemagen:
cargo run --package komorebic -- static-config-schema > schema.json
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
cargo run --package komorebi-bar -- --schema > schema.bar.json
komorebic static-config-schema > schema.json
generate-schema-doc .\schema.json --config template_name=js_offline --config minify=false .\static-config-docs\
generate-schema-doc .\schema.bar.json --config template_name=js_offline --config minify=false .\bar-config-docs\
rm -Force .\bar-config-docs\schema.html
mv .\bar-config-docs\schema.bar.html .\bar-config-docs\schema.html

View File

@@ -1,37 +0,0 @@
[package]
name = "komorebi-bar"
version = "0.1.30"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-client = { path = "../komorebi-client" }
komorebi-themes = { path = "../komorebi-themes" }
chrono = { workspace = true }
clap = { workspace = true }
color-eyre = { workspace = true }
crossbeam-channel = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
eframe = { workspace = true }
egui-phosphor = "0.7"
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
netdev = "0.31"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
random_word = { version = "0.4", features = ["en"] }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
starship-battery = "0.10"
sysinfo = { workspace = true }
tracing = { workspace = true }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
windows = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }

View File

@@ -1,458 +0,0 @@
use crate::config::KomobarConfig;
use crate::config::KomobarTheme;
use crate::config::Position;
use crate::config::PositionConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiNotificationState;
use crate::process_hwnd;
use crate::widget::BarWidget;
use crate::widget::WidgetConfig;
use crate::BAR_HEIGHT;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_LEFT;
use crate::MONITOR_RIGHT;
use crate::MONITOR_TOP;
use crossbeam_channel::Receiver;
use eframe::egui::Align;
use eframe::egui::CentralPanel;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::FontData;
use eframe::egui::FontDefinitions;
use eframe::egui::FontFamily;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Layout;
use eframe::egui::Margin;
use eframe::egui::Style;
use eframe::egui::TextStyle;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::KomorebiTheme;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
use komorebi_themes::Catppuccin;
use komorebi_themes::CatppuccinValue;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub struct Komobar {
pub config: Arc<KomobarConfig>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>,
pub rx_gui: Receiver<komorebi_client::Notification>,
pub rx_config: Receiver<KomobarConfig>,
pub bg_color: Rc<RefCell<Color32>>,
pub scale_factor: f32,
}
pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Color32>>) {
match theme {
KomobarTheme::Catppuccin {
name: catppuccin,
accent: catppuccin_value,
} => match catppuccin {
Catppuccin::Frappe => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::FRAPPE.base);
}
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
},
KomobarTheme::Base16 {
name: base16,
accent: base16_value,
} => {
ctx.set_style(base16.style());
let base16_value = base16_value.unwrap_or_default();
let accent = base16_value.color32(base16);
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
});
bg_color.replace(base16.background());
}
}
}
impl Komobar {
pub fn apply_config(
&mut self,
ctx: &Context,
config: &KomobarConfig,
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
) {
MAX_LABEL_WIDTH.store(
config.max_label_width.unwrap_or(400.0) as i32,
Ordering::SeqCst,
);
if let Some(font_family) = &config.font_family {
tracing::info!("attempting to add custom font family: {font_family}");
Self::add_custom_font(ctx, font_family);
}
let position = config.position.clone().unwrap_or(PositionConfig {
start: Some(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
}),
end: Some(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
}),
});
if let Some(hwnd) = process_hwnd() {
let start = position.start.unwrap_or(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
});
let end = position.end.unwrap_or(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
});
let rect = komorebi_client::Rect {
left: start.x as i32,
top: start.y as i32,
right: end.x as i32,
bottom: end.y as i32,
};
let window = komorebi_client::Window::from(hwnd);
match window.set_position(&rect, false) {
Ok(_) => {
tracing::info!("updated bar position");
}
Err(error) => {
tracing::error!("{}", error.to_string())
}
}
}
match config.theme {
Some(theme) => {
apply_theme(ctx, theme, self.bg_color.clone());
}
None => {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
);
let config = home_dir.join("komorebi.json");
match komorebi_client::StaticConfig::read(&config) {
Ok(config) => {
if let Some(theme) = config.theme {
apply_theme(ctx, KomobarTheme::from(theme), self.bg_color.clone());
let stack_accent = match theme {
KomorebiTheme::Catppuccin {
name, stack_border, ..
} => stack_border
.unwrap_or(CatppuccinValue::Green)
.color32(name.as_theme()),
KomorebiTheme::Base16 {
name, stack_border, ..
} => stack_border.unwrap_or(Base16Value::Base0B).color32(name),
};
if let Some(state) = &self.komorebi_notification_state {
state.borrow_mut().stack_accent = Some(stack_accent);
}
}
}
Err(_) => {
ctx.set_style(Style::default());
self.bg_color.replace(Style::default().visuals.panel_fill);
}
}
}
}
if let Some(font_size) = &config.font_size {
tracing::info!("attempting to set custom font size: {font_size}");
Self::set_font_size(ctx, *font_size);
}
let mut komorebi_widget = None;
let mut komorebi_widget_idx = None;
let mut komorebi_notification_state = previous_notification_state;
let mut side = None;
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Side::Left);
}
}
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Side::Right);
}
}
let mut left_widgets = config
.left_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
let mut right_widgets = config
.right_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
if let (Some(idx), Some(mut widget), Some(side)) =
(komorebi_widget_idx, komorebi_widget, side)
{
match komorebi_notification_state {
None => {
komorebi_notification_state = Some(widget.komorebi_notification_state.clone());
}
Some(ref previous) => {
previous
.borrow_mut()
.update_from_config(&widget.komorebi_notification_state.borrow());
widget.komorebi_notification_state = previous.clone();
}
}
let boxed: Box<dyn BarWidget> = Box::new(widget);
match side {
Side::Left => left_widgets[idx] = boxed,
Side::Right => right_widgets[idx] = boxed,
}
}
right_widgets.reverse();
self.left_widgets = left_widgets;
self.right_widgets = right_widgets;
tracing::info!("widget configuration options applied");
self.komorebi_notification_state = komorebi_notification_state;
}
pub fn new(
cc: &eframe::CreationContext<'_>,
rx_gui: Receiver<komorebi_client::Notification>,
rx_config: Receiver<KomobarConfig>,
config: Arc<KomobarConfig>,
) -> Self {
let mut komobar = Self {
config: config.clone(),
komorebi_notification_state: None,
left_widgets: vec![],
right_widgets: vec![],
rx_gui,
rx_config,
bg_color: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
};
komobar.apply_config(&cc.egui_ctx, &config, None);
// needs a double apply the first time for some reason
komobar.apply_config(&cc.egui_ctx, &config, None);
komobar
}
fn set_font_size(ctx: &Context, font_size: f32) {
ctx.style_mut(|style| {
style.text_styles = [
(TextStyle::Small, FontId::new(9.0, FontFamily::Proportional)),
(
TextStyle::Body,
FontId::new(font_size, FontFamily::Proportional),
),
(
TextStyle::Button,
FontId::new(font_size, FontFamily::Proportional),
),
(
TextStyle::Heading,
FontId::new(18.0, FontFamily::Proportional),
),
(
TextStyle::Monospace,
FontId::new(font_size, FontFamily::Monospace),
),
]
.into();
});
}
fn add_custom_font(ctx: &Context, name: &str) {
let mut fonts = FontDefinitions::default();
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
let property = FontPropertyBuilder::new().family(name).build();
if let Some((font, _)) = system_fonts::get(&property) {
fonts
.font_data
.insert(name.to_owned(), FontData::from_owned(font));
fonts
.families
.entry(FontFamily::Proportional)
.or_default()
.insert(0, name.to_owned());
fonts
.families
.entry(FontFamily::Monospace)
.or_default()
.push(name.to_owned());
// Tell egui to use these fonts:
ctx.set_fonts(fonts);
}
}
}
impl eframe::App for Komobar {
// TODO: I think this is needed for transparency??
// fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
// egui::Rgba::TRANSPARENT.to_array()
// let mut background = Color32::from_gray(18).to_normalized_gamma_f32();
// background[3] = 0.9;
// background
// }
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
self.apply_config(
ctx,
&self.config.clone(),
self.komorebi_notification_state.clone(),
);
}
if let Ok(updated_config) = self.rx_config.try_recv() {
self.apply_config(
ctx,
&updated_config,
self.komorebi_notification_state.clone(),
);
}
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
komorebi_notification_state
.borrow_mut()
.handle_notification(
ctx,
self.config.monitor.index,
self.rx_gui.clone(),
self.bg_color.clone(),
);
}
let frame = if let Some(frame) = &self.config.frame {
Frame::none()
.inner_margin(Margin::symmetric(
frame.inner_margin.x,
frame.inner_margin.y,
))
.fill(*self.bg_color.borrow())
} else {
Frame::none().fill(*self.bg_color.borrow())
};
CentralPanel::default().frame(frame).show(ctx, |ui| {
ui.horizontal_centered(|ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui);
}
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui);
}
})
})
});
}
}
#[derive(Copy, Clone)]
enum Side {
Left,
Right,
}

View File

@@ -1,160 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use starship_battery::units::ratio::percent;
use starship_battery::Manager;
use starship_battery::State;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct BatteryConfig {
/// Enable the Battery widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<BatteryConfig> for Battery {
fn from(value: BatteryConfig) -> Self {
let manager = Manager::new().unwrap();
let mut last_state = String::new();
let mut state = None;
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(mut batteries) = manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
match first.state() {
State::Charging => state = Some(BatteryState::Charging),
State::Discharging => state = Some(BatteryState::Discharging),
_ => {}
}
last_state = match prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
Self {
enable: value.enable,
manager,
last_state,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
state: state.unwrap_or(BatteryState::Discharging),
last_updated: Instant::now(),
}
}
}
pub enum BatteryState {
Charging,
Discharging,
}
pub struct Battery {
pub enable: bool,
manager: Manager,
pub state: BatteryState,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_state: String,
last_updated: Instant,
}
impl Battery {
fn output(&mut self) -> String {
let mut output = self.last_state.clone();
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
output.clear();
if let Ok(mut batteries) = self.manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
match first.state() {
State::Charging => self.state = BatteryState::Charging,
State::Discharging => self.state = BatteryState::Discharging,
_ => {}
}
output = match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
self.last_state.clone_from(&output);
self.last_updated = now;
}
output
}
}
impl BarWidget for Battery {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let emoji = match self.state {
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
};
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
ui.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
);
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,178 +0,0 @@
use crate::widget::WidgetConfig;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
use eframe::egui::Vec2;
use komorebi_client::KomorebiTheme;
use komorebi_client::Rect;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.30`
pub struct KomobarConfig {
/// Bar positioning options
#[serde(alias = "viewport")]
pub position: Option<PositionConfig>,
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// Monitor options
pub monitor: MonitorConfig,
/// Font family
pub font_family: Option<String>,
/// Font size (default: 12.5)
pub font_size: Option<f32>,
/// Max label width before text truncation (default: 400.0)
pub max_label_width: Option<f32>,
/// Theme
pub theme: Option<KomobarTheme>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Right side widgets (ordered left-to-right)
pub right_widgets: Vec<WidgetConfig>,
}
impl KomobarConfig {
pub fn aliases(raw: &str) {
let mut map = HashMap::new();
map.insert("position", ["viewport"]);
map.insert("end", ["inner_frame"]);
let mut display = false;
for aliases in map.values() {
for a in aliases {
if raw.contains(a) {
display = true;
}
}
}
if display {
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
for (canonical, aliases) in map {
for alias in aliases {
if raw.contains(alias) {
println!(r#""{alias}" is now "{canonical}""#);
}
}
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct PositionConfig {
/// The desired starting position of the bar (0,0 = top left of the screen)
#[serde(alias = "position")]
pub start: Option<Position>,
/// The desired size of the bar from the starting position (usually monitor width x desired height)
#[serde(alias = "inner_size")]
pub end: Option<Position>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct FrameConfig {
/// Margin inside the painted frame
pub inner_margin: Position,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MonitorConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub index: usize,
/// Automatically apply a work area offset for this monitor to accommodate the bar
pub work_area_offset: Option<Rect>,
}
impl KomobarConfig {
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = match path.extension().unwrap().to_string_lossy().as_str() {
"json" => serde_json::from_str(&content)?,
_ => panic!("unsupported format"),
};
if value.frame.is_none() {
value.frame = Some(FrameConfig {
inner_margin: Position { x: 10.0, y: 10.0 },
});
}
Ok(value)
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct Position {
/// X coordinate
pub x: f32,
/// Y coordinate
pub y: f32,
}
impl From<Position> for Vec2 {
fn from(value: Position) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
impl From<Position> for Pos2 {
fn from(value: Position) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "palette")]
pub enum KomobarTheme {
/// A theme from catppuccin-egui
Catppuccin {
name: komorebi_themes::Catppuccin,
accent: Option<komorebi_themes::CatppuccinValue>,
},
/// A theme from base16-egui-themes
Base16 {
name: komorebi_themes::Base16,
accent: Option<komorebi_themes::Base16Value>,
},
}
impl From<KomorebiTheme> for KomobarTheme {
fn from(value: KomorebiTheme) -> Self {
match value {
KomorebiTheme::Catppuccin {
name, bar_accent, ..
} => Self::Catppuccin {
name,
accent: bar_accent,
},
KomorebiTheme::Base16 {
name, bar_accent, ..
} => Self::Base16 {
name,
accent: bar_accent,
},
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum LabelPrefix {
/// Show no prefix
None,
/// Show an icon
Icon,
/// Show text
Text,
/// Show an icon and text
IconAndText,
}

View File

@@ -1,120 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct CpuConfig {
/// Enable the Cpu widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<CpuConfig> for Cpu {
fn from(value: CpuConfig) -> Self {
let mut system =
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
system.refresh_cpu_usage();
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Cpu {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Cpu {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_cpu_usage();
self.last_updated = now;
}
let used = self.system.global_cpu_usage();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
}
}
}
impl BarWidget for Cpu {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CPU.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,137 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct DateConfig {
/// Enable the Date widget
pub enable: bool,
/// Set the Date format
pub format: DateFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<DateConfig> for Date {
fn from(value: DateConfig) -> Self {
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum DateFormat {
/// Month/Date/Year format (09/08/24)
MonthDateYear,
/// Year-Month-Date format (2024-09-08)
YearMonthDate,
/// Date-Month-Year format (8-Sep-2024)
DateMonthYear,
/// Day Date Month Year format (8 September 2024)
DayDateMonthYear,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
Custom(String),
}
impl DateFormat {
pub fn next(&mut self) {
match self {
DateFormat::MonthDateYear => *self = Self::YearMonthDate,
DateFormat::YearMonthDate => *self = Self::DateMonthYear,
DateFormat::DateMonthYear => *self = Self::DayDateMonthYear,
DateFormat::DayDateMonthYear => *self = Self::MonthDateYear,
_ => {}
};
}
fn fmt_string(&self) -> String {
match self {
DateFormat::MonthDateYear => String::from("%D"),
DateFormat::YearMonthDate => String::from("%F"),
DateFormat::DateMonthYear => String::from("%v"),
DateFormat::DayDateMonthYear => String::from("%A %e %B %Y"),
DateFormat::Custom(custom) => custom.to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct Date {
pub enable: bool,
pub format: DateFormat,
label_prefix: LabelPrefix,
}
impl Date {
fn output(&mut self) -> String {
chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()
}
}
impl BarWidget for Date {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let mut output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CALENDAR_DOTS.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.insert_str(0, "DATE: ");
}
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.next()
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,546 +0,0 @@
use crate::bar::apply_theme;
use crate::config::KomobarTheme;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use crate::WIDGET_SPACING;
use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError;
use eframe::egui::text::LayoutJob;
use eframe::egui::Color32;
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::SelectableLabel;
use eframe::egui::Sense;
use eframe::egui::TextStyle;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use image::RgbaImage;
use komorebi_client::CycleDirection;
use komorebi_client::NotificationEvent;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::fmt::Formatter;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: KomorebiWorkspacesConfig,
/// Configure the Layout widget
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Focused Window widget
pub focused_window: Option<KomorebiFocusedWindowConfig>,
/// Configure the Configuration Switcher widget
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiWorkspacesConfig {
/// Enable the Komorebi Workspaces widget
pub enable: bool,
/// Hide workspaces without any windows
pub hide_empty_workspaces: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiFocusedWindowConfig {
/// Enable the Komorebi Focused Window widget
pub enable: bool,
/// Show the icon of the currently focused window
pub show_icon: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfigurationSwitcherConfig {
/// Enable the Komorebi Configurations widget
pub enable: bool,
/// A map of display friendly name => path to configuration.json
pub configurations: BTreeMap<String, String>,
}
impl From<&KomorebiConfig> for Komorebi {
fn from(value: &KomorebiConfig) -> Self {
let configuration_switcher =
if let Some(configuration_switcher) = &value.configuration_switcher {
let mut configuration_switcher = configuration_switcher.clone();
for (_, location) in configuration_switcher.configurations.iter_mut() {
*location = dunce::simplified(&PathBuf::from(location.clone()))
.to_string_lossy()
.to_string();
}
Some(configuration_switcher)
} else {
None
};
Self {
komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState {
selected_workspace: String::new(),
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
workspaces: vec![],
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
mouse_follows_focus: true,
work_area_offset: None,
focused_container_information: (vec![], vec![], 0),
stack_accent: None,
})),
workspaces: value.workspaces,
layout: value.layout,
focused_window: value.focused_window,
configuration_switcher,
}
}
}
#[derive(Clone, Debug)]
pub struct Komorebi {
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
pub workspaces: KomorebiWorkspacesConfig,
pub layout: Option<KomorebiLayoutConfig>,
pub focused_window: Option<KomorebiFocusedWindowConfig>,
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
impl BarWidget for Komorebi {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
if self.workspaces.enable {
let mut update = None;
for (i, ws) in komorebi_notification_state.workspaces.iter().enumerate() {
if ui
.add(SelectableLabel::new(
komorebi_notification_state.selected_workspace.eq(ws),
ws.to_string(),
))
.clicked()
{
update = Some(ws.to_string());
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
.is_err()
{
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i))
.is_err()
{
tracing::error!("could not send message to komorebi: FocusWorkspaceNumber");
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::RetileWithResizeDimensions)
.is_err()
{
tracing::error!("could not send message to komorebi: Retile");
}
}
}
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
ui.add_space(WIDGET_SPACING);
}
if let Some(layout) = self.layout {
if layout.enable {
if ui
.add(
Label::new(komorebi_notification_state.layout.to_string())
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
match komorebi_notification_state.layout {
KomorebiLayout::Default(_) => {
if komorebi_client::send_message(&SocketMessage::CycleLayout(
CycleDirection::Next,
))
.is_err()
{
tracing::error!("could not send message to komorebi: CycleLayout");
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err()
{
tracing::error!("could not send message to komorebi: ToggleTiling");
}
}
KomorebiLayout::Paused => {
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
tracing::error!("could not send message to komorebi: TogglePause");
}
}
KomorebiLayout::Custom => {}
}
}
ui.add_space(WIDGET_SPACING);
}
}
if let Some(configuration_switcher) = &self.configuration_switcher {
if configuration_switcher.enable {
for (name, location) in configuration_switcher.configurations.iter() {
let path = PathBuf::from(location);
if path.is_file()
&& ui
.add(Label::new(name).selectable(false).sense(Sense::click()))
.clicked()
{
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
canonicalized,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: ReplaceConfiguration"
);
proceed = false;
}
if let Some(rect) = komorebi_notification_state.work_area_offset {
if proceed {
match komorebi_client::send_query(&SocketMessage::Query(
komorebi_client::StateQuery::FocusedMonitorIndex,
)) {
Ok(idx) => {
if let Ok(monitor_idx) = idx.parse::<usize>() {
if komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
rect,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: MonitorWorkAreaOffset"
);
}
}
}
Err(_) => {
tracing::error!(
"could not send message to komorebi: Query"
);
}
}
}
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
if let Some(focused_window) = self.focused_window {
if focused_window.enable {
let titles = &komorebi_notification_state.focused_container_information.0;
let icons = &komorebi_notification_state.focused_container_information.1;
let focused_window_idx =
komorebi_notification_state.focused_container_information.2;
let iter = titles.iter().zip(icons.iter());
for (i, (title, icon)) in iter.enumerate() {
if focused_window.show_icon {
if let Some(img) = icon {
ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.max_height(15.0),
);
}
}
if i == focused_window_idx {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let layout_job = LayoutJob::simple(
title.to_string(),
font_id.clone(),
komorebi_notification_state
.stack_accent
.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
if titles.len() > 1 {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job).selectable(false).truncate(),
);
} else {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title).selectable(false).truncate(),
);
}
} else {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
{
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
false,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(i))
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusStackWindow"
);
}
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
ui.add_space(WIDGET_SPACING);
}
}
}
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
ctx.load_texture("icon", color_image, TextureOptions::default())
}
#[derive(Clone, Debug)]
pub struct KomorebiNotificationState {
pub workspaces: Vec<String>,
pub selected_workspace: String,
pub focused_container_information: (Vec<String>, Vec<Option<RgbaImage>>, usize),
pub layout: KomorebiLayout,
pub hide_empty_workspaces: bool,
pub mouse_follows_focus: bool,
pub work_area_offset: Option<Rect>,
pub stack_accent: Option<Color32>,
}
#[derive(Copy, Clone, Debug)]
pub enum KomorebiLayout {
Default(komorebi_client::DefaultLayout),
Floating,
Paused,
Custom,
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
}
impl KomorebiNotificationState {
pub fn update_from_config(&mut self, config: &Self) {
self.hide_empty_workspaces = config.hide_empty_workspaces;
}
pub fn handle_notification(
&mut self,
ctx: &Context,
monitor_index: usize,
rx_gui: Receiver<komorebi_client::Notification>,
bg_color: Rc<RefCell<Color32>>,
) {
match rx_gui.try_recv() {
Err(error) => match error {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
tracing::error!(
"failed to receive komorebi notification on gui thread: {error}"
);
}
},
Ok(notification) => {
if let NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(path)) =
notification.event
{
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
tracing::info!("applied theme from updated komorebi.json");
}
}
}
self.mouse_follows_focus = notification.state.mouse_follows_focus;
let monitor = &notification.state.monitors.elements()[monitor_index];
self.work_area_offset =
notification.state.monitors.elements()[monitor_index].work_area_offset();
let focused_workspace_idx = monitor.focused_workspace_idx();
let mut workspaces = vec![];
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
.name()
.to_owned()
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
for (i, ws) in monitor.workspaces().iter().enumerate() {
let should_add = if self.hide_empty_workspaces {
focused_workspace_idx == i || !ws.containers().is_empty()
} else {
true
};
if should_add {
workspaces
.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)));
}
}
self.workspaces = workspaces;
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
};
if !*monitor.workspaces()[focused_workspace_idx].tile() {
self.layout = KomorebiLayout::Floating;
}
if notification.state.is_paused {
self.layout = KomorebiLayout::Paused;
}
if let Some(container) =
monitor.workspaces()[focused_workspace_idx].monocle_container()
{
self.focused_container_information = (
container
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
container
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
container.focused_window_idx(),
);
} else if let Some(container) =
monitor.workspaces()[focused_workspace_idx].focused_container()
{
self.focused_container_information = (
container
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
container
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
container.focused_window_idx(),
);
} else {
self.focused_container_information.0.clear();
self.focused_container_information.1.clear();
self.focused_container_information.2 = 0;
}
}
}
}
}

View File

@@ -1,411 +0,0 @@
mod bar;
mod battery;
mod config;
mod cpu;
mod date;
mod komorebi;
mod media;
mod memory;
mod network;
mod storage;
mod time;
mod ui;
mod widget;
use crate::bar::Komobar;
use crate::config::KomobarConfig;
use crate::config::Position;
use crate::config::PositionConfig;
use clap::Parser;
use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use schemars::gen::SchemaSettings;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
pub static WIDGET_SPACING: f32 = 10.0;
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
#[derive(Parser)]
#[clap(author, about, version)]
struct Opts {
/// Print the JSON schema of the configuration file and exit
#[clap(long)]
schema: bool,
/// Print a list of fonts available on this system and exit
#[clap(long)]
fonts: bool,
/// Path to a JSON or YAML configuration file
#[clap(short, long)]
config: Option<PathBuf>,
/// Write an example komorebi.bar.json to disk
#[clap(long)]
quickstart: bool,
/// Print a list of aliases that can be renamed to canonical variants
#[clap(long)]
#[clap(hide = true)]
aliases: bool,
}
extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
unsafe {
let mut process_id = 0;
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
if process_id == GetCurrentProcessId() {
*(lparam.0 as *mut HWND) = hwnd;
BOOL::from(false) // Stop enumeration
} else {
BOOL::from(true) // Continue enumeration
}
}
}
fn process_hwnd() -> Option<isize> {
unsafe {
let mut hwnd = HWND::default();
let _ = EnumThreadWindows(
GetCurrentThreadId(),
Some(enum_window),
LPARAM(&mut hwnd as *mut HWND as isize),
);
if hwnd.0 as isize == 0 {
None
} else {
Some(hwnd.0 as isize)
}
}
}
fn main() -> color_eyre::Result<()> {
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }?;
let opts: Opts = Opts::parse();
if opts.schema {
let settings = SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
println!("{schema}");
std::process::exit(0);
}
if opts.fonts {
for font in system_fonts::query_all() {
println!("{font}");
}
std::process::exit(0);
}
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
color_eyre::install()?;
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
tracing::subscriber::set_global_default(
tracing_subscriber::fmt::Subscriber::builder()
.with_env_filter(EnvFilter::from_default_env())
.finish(),
)?;
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
);
if opts.quickstart {
let komorebi_bar_json = include_str!("../../docs/komorebi.bar.example.json").to_string();
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
println!(
"Example komorebi.bar.json file written to {}",
home_dir.as_path().display()
);
std::process::exit(0);
}
let default_config_path = home_dir.join("komorebi.bar.json");
let config_path = opts.config.map_or_else(
|| {
if !default_config_path.is_file() {
None
} else {
Some(default_config_path.clone())
}
},
Option::from,
);
let mut config = match config_path {
None => {
let komorebi_bar_json =
include_str!("../../docs/komorebi.bar.example.json").to_string();
std::fs::write(&default_config_path, komorebi_bar_json)?;
tracing::info!(
"created example configuration file: {}",
default_config_path.as_path().display()
);
KomobarConfig::read(&default_config_path)?
}
Some(ref config) => {
if !opts.aliases {
tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
}
KomobarConfig::read(config)?
}
};
let config_path = config_path.unwrap_or(default_config_path);
if opts.aliases {
KomobarConfig::aliases(&std::fs::read_to_string(&config_path)?);
std::process::exit(0);
}
let state = serde_json::from_str::<komorebi_client::State>(&komorebi_client::send_query(
&SocketMessage::State,
)?)?;
MONITOR_RIGHT.store(
state.monitors.elements()[config.monitor.index].size().right,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().top,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().left,
Ordering::SeqCst,
);
match config.position {
None => {
config.position = Some(PositionConfig {
start: Some(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
}),
end: Some(Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 50.0,
}),
})
}
Some(ref mut position) => {
if position.start.is_none() {
position.start = Some(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
});
}
if position.end.is_none() {
position.end = Some(Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 50.0,
})
}
}
}
let viewport_builder = ViewportBuilder::default()
.with_decorations(false)
// .with_transparent(config.transparent)
.with_taskbar(false);
let native_options = eframe::NativeOptions {
viewport: viewport_builder,
..Default::default()
};
if let Some(rect) = &config.monitor.work_area_offset {
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
config.monitor.index,
*rect,
))?;
tracing::info!(
"work area offset applied to monitor: {}",
config.monitor.index
);
}
let (tx_gui, rx_gui) = crossbeam_channel::unbounded();
let (tx_config, rx_config) = crossbeam_channel::unbounded();
let mut hotwatch = Hotwatch::new()?;
let config_path_cl = config_path.clone();
hotwatch.watch(config_path, move |event| match event.kind {
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
Ok(updated) => {
tracing::info!(
"configuration file updated: {}",
config_path_cl.as_path().to_string_lossy()
);
if let Err(error) = tx_config.send(updated) {
tracing::error!("could not send configuration update to gui: {error}")
}
}
Err(error) => {
tracing::error!("{error}");
}
},
_ => {}
})?;
tracing::info!("watching configuration file for changes");
let config_arc = Arc::new(config);
eframe::run_native(
"komorebi-bar",
native_options,
Box::new(|cc| {
let config_cl = config_arc.clone();
let ctx_repainter = cc.egui_ctx.clone();
std::thread::spawn(move || loop {
std::thread::sleep(Duration::from_secs(1));
ctx_repainter.request_repaint();
});
let ctx_komorebi = cc.egui_ctx.clone();
std::thread::spawn(move || {
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
filter_state_changes: true,
})
.expect("could not subscribe to komorebi notifications");
tracing::info!("subscribed to komorebi notifications: \"{}\"", subscriber_name);
for client in listener.incoming() {
match client {
Ok(subscription) => {
let mut buffer = Vec::new();
let mut reader = BufReader::new(subscription);
// this is when we know a shutdown has been sent
if matches!(reader.read_to_end(&mut buffer), Ok(0)) {
tracing::info!("disconnected from komorebi");
// keep trying to reconnect to komorebi
while komorebi_client::send_message(
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
)
.is_err()
{
std::thread::sleep(Duration::from_secs(1));
}
tracing::info!("reconnected to komorebi");
if let Some(rect) = &config_cl.monitor.work_area_offset {
while komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
config_cl.monitor.index,
*rect,
),
)
.is_err()
{
std::thread::sleep(Duration::from_secs(1));
}
}
}
match String::from_utf8(buffer) {
Ok(notification_string) => {
match serde_json::from_str::<komorebi_client::Notification>(
&notification_string,
) {
Ok(notification) => {
tracing::debug!("received notification from komorebi");
if let Err(error) = tx_gui.send(notification) {
tracing::error!("could not send komorebi notification update to gui thread: {error}")
}
ctx_komorebi.request_repaint();
}
Err(error) => {
tracing::error!("could not deserialize komorebi notification: {error}");
}
}
}
Err(error) => {
tracing::error!(
"komorebi notification string was invalid utf8: {error}"
)
}
}
}
Err(error) => {
tracing::error!("{error}");
}
}
}
});
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config_arc)))
}),
)
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
}

View File

@@ -1,128 +0,0 @@
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,
}
impl From<MediaConfig> for Media {
fn from(value: MediaConfig) -> Self {
Self::new(value.enable)
}
}
#[derive(Clone, Debug)]
pub struct Media {
pub enable: bool,
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
}
impl Media {
pub fn new(enable: bool) -> Self {
Self {
enable,
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
.unwrap()
.get()
.unwrap(),
}
}
pub fn toggle(&self) {
if let Ok(session) = self.session_manager.GetCurrentSession() {
if let Ok(op) = session.TryTogglePlayPauseAsync() {
op.get().unwrap_or_default();
}
}
}
fn output(&mut self) -> String {
if let Ok(session) = self.session_manager.GetCurrentSession() {
if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
if let Ok(properties) = operation.get() {
if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
if artist.is_empty() {
return format!("{title}");
}
if title.is_empty() {
return format!("{artist}");
}
return format!("{artist} - {title}");
}
}
}
}
String::new()
}
}
impl BarWidget for Media {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
egui_phosphor::regular::HEADPHONES.to_string(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
{
self.toggle();
}
ui.add_space(WIDGET_SPACING);
}
}
}
}

View File

@@ -1,123 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MemoryConfig {
/// Enable the Memory widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<MemoryConfig> for Memory {
fn from(value: MemoryConfig) -> Self {
let mut system =
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
system.refresh_memory();
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Memory {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Memory {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_memory();
self.last_updated = now;
}
let used = self.system.used_memory();
let total = self.system.total_memory();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {}%", (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
}
}
}
impl BarWidget for Memory {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::MEMORY.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,425 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use num_derive::FromPrimitive;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show total data transmitted
pub show_total_data_transmitted: bool,
/// Show network activity
pub show_network_activity: bool,
/// Characters to reserve for network activity data
pub network_activity_fill_characters: Option<usize>,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let mut last_state_data = vec![];
let mut last_state_transmitted = vec![];
let mut networks_total_data_transmitted = Networks::new_with_refreshed_list();
let mut networks_network_activity = Networks::new_with_refreshed_list();
let mut default_interface = String::new();
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = interface.friendly_name {
default_interface.clone_from(&friendly_name);
if value.show_total_data_transmitted {
networks_total_data_transmitted.refresh();
for (interface_name, data) in &networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
last_state_data.push(match prefix {
LabelPrefix::None => format!(
"{} | {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Icon => format!(
"{} {} | {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Text => format!(
"\u{2211}DOWN: {} | \u{2211}UP: {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::IconAndText => format!(
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
})
}
}
}
if value.show_network_activity {
networks_network_activity.refresh();
for (interface_name, data) in &networks_network_activity {
if friendly_name.eq(interface_name) {
last_state_transmitted.push(match prefix {
LabelPrefix::None => format!(
"{: >width$}/s | {: >width$}/s",
to_pretty_bytes(data.received(), 1),
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::Icon => format!(
"{} {: >width$}/s | {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::Text => format!(
"DOWN: {: >width$}/s | UP: {: >width$}/s",
to_pretty_bytes(data.received(), 1),
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::IconAndText => format!(
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
})
}
}
}
}
}
Self {
enable: value.enable,
networks_total_data_transmitted,
networks_network_activity,
default_interface,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
show_total_data_transmitted: value.show_total_data_transmitted,
show_network_activity: value.show_network_activity,
network_activity_fill_characters: value
.network_activity_fill_characters
.unwrap_or_default(),
last_state_total_data_transmitted: last_state_data,
last_state_network_activity: last_state_transmitted,
last_updated_total_data_transmitted: Instant::now(),
last_updated_network_activity: Instant::now(),
}
}
}
pub struct Network {
pub enable: bool,
pub show_total_data_transmitted: bool,
pub show_network_activity: bool,
networks_total_data_transmitted: Networks,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
default_interface: String,
last_state_total_data_transmitted: Vec<String>,
last_state_network_activity: Vec<String>,
last_updated_total_data_transmitted: Instant,
last_updated_network_activity: Instant,
network_activity_fill_characters: usize,
}
impl Network {
fn default_interface(&mut self) {
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
}
}
}
fn network_activity(&mut self) -> Vec<String> {
let mut outputs = self.last_state_network_activity.clone();
let now = Instant::now();
if self.show_network_activity
&& now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_network_activity {
self.networks_network_activity.refresh();
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
outputs.push(match self.label_prefix {
LabelPrefix::None => format!(
"{: >width$}/s | {: >width$}/s",
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::Icon => format!(
"{} {: >width$}/s | {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::Text => format!(
"DOWN: {: >width$}/s | UP: {: >width$}/s",
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::IconAndText => {
format!(
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
)
}
})
}
}
}
}
}
self.last_state_network_activity.clone_from(&outputs);
self.last_updated_network_activity = now;
}
outputs
}
fn total_data_transmitted(&mut self) -> Vec<String> {
let mut outputs = self.last_state_total_data_transmitted.clone();
let now = Instant::now();
if self.show_total_data_transmitted
&& now.duration_since(self.last_updated_total_data_transmitted)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_total_data_transmitted {
self.networks_total_data_transmitted.refresh();
for (interface_name, data) in &self.networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
outputs.push(match self.label_prefix {
LabelPrefix::None => format!(
"{} | {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Icon => format!(
"{} {} | {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Text => format!(
"\u{2211}DOWN: {} | \u{2211}UP: {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::IconAndText => format!(
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
})
}
}
}
}
}
self.last_state_total_data_transmitted.clone_from(&outputs);
self.last_updated_total_data_transmitted = now;
}
outputs
}
}
impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.show_total_data_transmitted {
for output in self.total_data_transmitted() {
ui.add(Label::new(output).selectable(false));
}
ui.add_space(WIDGET_SPACING);
}
if self.show_network_activity {
for output in self.network_activity() {
ui.add(Label::new(output).selectable(false));
}
ui.add_space(WIDGET_SPACING);
}
if self.enable {
self.default_interface();
if !self.default_interface.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error)
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
}
#[derive(Debug, FromPrimitive)]
enum DataUnit {
B = 0,
K = 1,
M = 2,
G = 3,
T = 4,
P = 5,
E = 6,
Z = 7,
Y = 8,
}
impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
}

View File

@@ -1,134 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Disks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct StorageConfig {
/// Enable the Storage widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<StorageConfig> for Storage {
fn from(value: StorageConfig) -> Self {
Self {
enable: value.enable,
disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Storage {
pub enable: bool,
disks: Disks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Storage {
fn output(&mut self) -> Vec<String> {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.disks.refresh();
self.last_updated = now;
}
let mut disks = vec![];
for disk in &self.disks {
let mount = disk.mount_point();
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
disks.push(match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
})
}
disks.sort();
disks.reverse();
disks
}
}
impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
for output in self.output() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::HARD_DRIVES.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
output.split(' ').collect::<Vec<&str>>()[0],
])
.spawn()
{
eprintln!("{}", error)
}
}
ui.add_space(WIDGET_SPACING);
}
}
}
}

View File

@@ -1,128 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct TimeConfig {
/// Enable the Time widget
pub enable: bool,
/// Set the Time format
pub format: TimeFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<TimeConfig> for Time {
fn from(value: TimeConfig) -> Self {
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum TimeFormat {
/// Twelve-hour format (with seconds)
TwelveHour,
/// Twenty-four-hour format (with seconds)
TwentyFourHour,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
Custom(String),
}
impl TimeFormat {
pub fn toggle(&mut self) {
match self {
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
_ => {}
};
}
fn fmt_string(&self) -> String {
match self {
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
TimeFormat::TwentyFourHour => String::from("%T"),
TimeFormat::Custom(format) => format.to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct Time {
pub enable: bool,
pub format: TimeFormat,
label_prefix: LabelPrefix,
}
impl Time {
fn output(&mut self) -> String {
chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()
}
}
impl BarWidget for Time {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let mut output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CLOCK.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.insert_str(0, "TIME: ");
}
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.toggle()
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,22 +0,0 @@
use eframe::egui::Align;
use eframe::egui::Layout;
use eframe::egui::Response;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::Widget;
pub struct CustomUi<'ui>(pub &'ui mut Ui);
impl CustomUi<'_> {
pub fn add_sized_left_to_right(
&mut self,
max_size: impl Into<Vec2>,
widget: impl Widget,
) -> Response {
let layout =
Layout::from_main_dir_and_cross_align(self.0.layout().main_dir(), Align::Center);
self.0
.allocate_ui_with_layout(max_size.into(), layout, |ui| ui.add(widget))
.inner
}
}

View File

@@ -1,56 +0,0 @@
use crate::battery::Battery;
use crate::battery::BatteryConfig;
use crate::cpu::Cpu;
use crate::cpu::CpuConfig;
use crate::date::Date;
use crate::date::DateConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiConfig;
use crate::media::Media;
use crate::media::MediaConfig;
use crate::memory::Memory;
use crate::memory::MemoryConfig;
use crate::network::Network;
use crate::network::NetworkConfig;
use crate::storage::Storage;
use crate::storage::StorageConfig;
use crate::time::Time;
use crate::time::TimeConfig;
use eframe::egui::Context;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
pub trait BarWidget {
fn render(&mut self, ctx: &Context, ui: &mut Ui);
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum WidgetConfig {
Battery(BatteryConfig),
Cpu(CpuConfig),
Date(DateConfig),
Komorebi(KomorebiConfig),
Media(MediaConfig),
Memory(MemoryConfig),
Network(NetworkConfig),
Storage(StorageConfig),
Time(TimeConfig),
}
impl WidgetConfig {
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
match self {
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
}
}
}

View File

@@ -1,12 +1,12 @@
[package]
name = "komorebi-client"
version = "0.1.30"
version = "0.1.27-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi = { path = "../komorebi" }
uds_windows = { workspace = true }
komorebi-core = { path = "../komorebi-core" }
uds_windows = "1"
serde_json = { workspace = true }

View File

@@ -1,36 +1,9 @@
#![warn(clippy::all)]
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::config_generation::ApplicationConfiguration;
pub use komorebi::container::Container;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::resolve_home_path;
pub use komorebi::core::AnimationStyle;
pub use komorebi::core::ApplicationIdentifier;
pub use komorebi::core::Arrangement;
pub use komorebi::core::Axis;
pub use komorebi::core::BorderImplementation;
pub use komorebi::core::BorderStyle;
pub use komorebi::core::CustomLayout;
pub use komorebi::core::CycleDirection;
pub use komorebi::core::DefaultLayout;
pub use komorebi::core::Direction;
pub use komorebi::core::FocusFollowsMouseImplementation;
pub use komorebi::core::HidingBehaviour;
pub use komorebi::core::Layout;
pub use komorebi::core::MoveBehaviour;
pub use komorebi::core::OperationBehaviour;
pub use komorebi::core::OperationDirection;
pub use komorebi::core::Rect;
pub use komorebi::core::Sizing;
pub use komorebi::core::SocketMessage;
pub use komorebi::core::StackbarLabel;
pub use komorebi::core::StackbarMode;
pub use komorebi::core::StateQuery;
pub use komorebi::core::WindowKind;
pub use komorebi::monitor::Monitor;
pub use komorebi::ring::Ring;
pub use komorebi::window::Window;
@@ -38,15 +11,27 @@ pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace;
pub use komorebi::BorderColours;
pub use komorebi::GlobalState;
pub use komorebi::KomorebiTheme;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
pub use komorebi_core::Arrangement;
pub use komorebi_core::Axis;
pub use komorebi_core::BorderStyle;
pub use komorebi_core::CustomLayout;
pub use komorebi_core::CycleDirection;
pub use komorebi_core::DefaultLayout;
pub use komorebi_core::Direction;
pub use komorebi_core::Layout;
pub use komorebi_core::OperationDirection;
pub use komorebi_core::Rect;
pub use komorebi_core::SocketMessage;
pub use komorebi_core::StackbarLabel;
pub use komorebi_core::StackbarMode;
pub use komorebi_core::WindowKind;
use komorebi::DATA_DIR;
@@ -61,10 +46,16 @@ const KOMOREBI: &str = "komorebi.sock";
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.write_all(serde_json::to_string(message)?.as_bytes())
}
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
}
}
Ok(())
}
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
let socket = DATA_DIR.join(KOMOREBI);
@@ -98,29 +89,3 @@ 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)
}

18
komorebi-core/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "komorebi-core"
version = "0.1.27-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = "0.9"
strum = { version = "0.26", features = ["derive"] }
schemars = "0.8"
color-eyre = { workspace = true }
windows = { workspace = true }
dunce = { workspace = true }
dirs = { workspace = true }

View File

@@ -7,12 +7,12 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use super::custom_layout::Column;
use super::custom_layout::ColumnSplit;
use super::custom_layout::ColumnSplitWithCapacity;
use super::CustomLayout;
use super::DefaultLayout;
use super::Rect;
use crate::custom_layout::Column;
use crate::custom_layout::ColumnSplit;
use crate::custom_layout::ColumnSplitWithCapacity;
use crate::CustomLayout;
use crate::DefaultLayout;
use crate::Rect;
pub trait Arrangement {
fn calculate(
@@ -604,16 +604,7 @@ impl Arrangement for CustomLayout {
}
#[derive(
Clone,
Copy,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum Axis {
Horizontal,

View File

@@ -6,7 +6,7 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use super::ApplicationIdentifier;
use crate::ApplicationIdentifier;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
@@ -59,14 +59,6 @@ pub enum MatchingRule {
Composite(Vec<IdWithIdentifier>),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceMatchingRule {
pub monitor_index: usize,
pub workspace_index: usize,
pub matching_rule: MatchingRule,
pub initial_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct IdWithIdentifier {
pub kind: ApplicationIdentifier,
@@ -116,8 +108,7 @@ pub struct ApplicationConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<ApplicationOptions>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "float_identifiers")]
pub ignore_identifiers: Option<Vec<MatchingRule>>,
pub float_identifiers: Option<Vec<MatchingRule>>,
}
impl ApplicationConfiguration {
@@ -188,7 +179,7 @@ impl ApplicationConfigurationGenerator {
let mut lines = vec![String::from("# Generated by komorebic.exe"), String::new()];
let mut ignore_rules = vec![];
let mut float_rules = vec![];
for app in cfgen {
lines.push(format!("# {}", app.name));
@@ -202,15 +193,15 @@ impl ApplicationConfigurationGenerator {
}
}
if let Some(ignore_identifiers) = app.ignore_identifiers {
for matching_rule in ignore_identifiers {
if let Some(float_identifiers) = app.float_identifiers {
for matching_rule in float_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 !ignore_rules.contains(&float_rule) {
ignore_rules.push(float_rule.clone());
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
// if let Some(comment) = float.comment {
// lines.push(format!("# {comment}"));
@@ -239,7 +230,7 @@ impl ApplicationConfigurationGenerator {
let mut lines = vec![String::from("; Generated by komorebic.exe"), String::new()];
let mut ignore_rules = vec![];
let mut float_rules = vec![];
for app in cfgen {
lines.push(format!("; {}", app.name));
@@ -253,8 +244,8 @@ impl ApplicationConfigurationGenerator {
}
}
if let Some(ignore_identifiers) = app.ignore_identifiers {
for matching_rule in ignore_identifiers {
if let Some(float_identifiers) = app.float_identifiers {
for matching_rule in float_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule = format!(
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
@@ -262,8 +253,8 @@ impl ApplicationConfigurationGenerator {
);
// Don't want to send duped signals especially as configs get larger
if !ignore_rules.contains(&float_rule) {
ignore_rules.push(float_rule.clone());
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
// if let Some(comment) = float.comment {
// lines.push(format!("; {comment}"));

View File

@@ -12,9 +12,9 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use super::Rect;
use crate::Rect;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct CustomLayout(Vec<Column>);
impl Deref for CustomLayout {
@@ -250,7 +250,7 @@ impl CustomLayout {
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "column", content = "configuration")]
pub enum Column {
Primary(Option<ColumnWidth>),
@@ -258,18 +258,18 @@ pub enum Column {
Tertiary(ColumnSplit),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ColumnWidth {
WidthPercentage(f32),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ColumnSplit {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ColumnSplitWithCapacity {
Horizontal(usize),
Vertical(usize),

View File

@@ -5,9 +5,9 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use super::OperationDirection;
use super::Rect;
use super::Sizing;
use crate::OperationDirection;
use crate::Rect;
use crate::Sizing;
#[derive(
Clone,
@@ -35,37 +35,6 @@ pub enum DefaultLayout {
}
impl DefaultLayout {
pub fn leftmost_index(&self, len: usize) -> usize {
match self {
Self::UltrawideVerticalStack | Self::RightMainVerticalStack => match len {
n if n > 1 => 1,
_ => 0,
},
DefaultLayout::BSP
| DefaultLayout::Columns
| DefaultLayout::Rows
| DefaultLayout::VerticalStack
| DefaultLayout::HorizontalStack
| DefaultLayout::Grid => 0,
}
}
pub fn rightmost_index(&self, len: usize) -> usize {
match self {
DefaultLayout::BSP
| DefaultLayout::Columns
| DefaultLayout::Rows
| DefaultLayout::VerticalStack
| DefaultLayout::HorizontalStack
| DefaultLayout::Grid => len.saturating_sub(1),
DefaultLayout::UltrawideVerticalStack => match len {
2 => 0,
_ => len.saturating_sub(1),
},
DefaultLayout::RightMainVerticalStack => 0,
}
}
#[must_use]
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
pub fn resize(
@@ -195,14 +164,14 @@ impl DefaultLayout {
#[must_use]
pub const fn cycle_previous(self) -> Self {
match self {
Self::RightMainVerticalStack => Self::Grid,
Self::Grid => Self::UltrawideVerticalStack,
Self::BSP => Self::UltrawideVerticalStack,
Self::UltrawideVerticalStack => Self::HorizontalStack,
Self::HorizontalStack => Self::VerticalStack,
Self::VerticalStack => Self::Rows,
Self::Rows => Self::Columns,
Self::Columns => Self::BSP,
Self::BSP => Self::RightMainVerticalStack,
Self::Columns => Self::Grid,
Self::Grid => Self::RightMainVerticalStack,
Self::RightMainVerticalStack => Self::BSP,
}
}
}

View File

@@ -1,9 +1,9 @@
use super::custom_layout::Column;
use super::custom_layout::ColumnSplit;
use super::custom_layout::ColumnSplitWithCapacity;
use super::custom_layout::CustomLayout;
use super::DefaultLayout;
use super::OperationDirection;
use crate::custom_layout::Column;
use crate::custom_layout::ColumnSplit;
use crate::custom_layout::ColumnSplitWithCapacity;
use crate::custom_layout::CustomLayout;
use crate::DefaultLayout;
use crate::OperationDirection;
pub trait Direction {
fn index_in_direction(
@@ -90,13 +90,9 @@ impl Direction for DefaultLayout {
idx: usize,
count: usize,
) -> bool {
if count < 2 {
return false;
}
match op_direction {
OperationDirection::Up => match self {
Self::BSP => idx != 0 && idx != 1,
Self::BSP => count > 2 && idx != 0 && idx != 1,
Self::Columns => false,
Self::Rows | Self::HorizontalStack => idx != 0,
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
@@ -104,7 +100,7 @@ impl Direction for DefaultLayout {
Self::Grid => !is_grid_edge(op_direction, idx, count),
},
OperationDirection::Down => match self {
Self::BSP => idx != count - 1 && idx % 2 != 0,
Self::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
Self::Columns => false,
Self::Rows => idx != count - 1,
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
@@ -113,22 +109,23 @@ impl Direction for DefaultLayout {
Self::Grid => !is_grid_edge(op_direction, idx, count),
},
OperationDirection::Left => match self {
Self::BSP => idx != 0,
Self::BSP => count > 1 && idx != 0,
Self::Columns | Self::VerticalStack => idx != 0,
Self::RightMainVerticalStack => idx == 0,
Self::Rows => false,
Self::HorizontalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx != 1,
Self::UltrawideVerticalStack => count > 1 && idx != 1,
Self::Grid => !is_grid_edge(op_direction, idx, count),
},
OperationDirection::Right => match self {
Self::BSP => idx % 2 == 0 && idx != count - 1,
Self::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
Self::Columns => idx != count - 1,
Self::Rows => false,
Self::VerticalStack => idx == 0,
Self::RightMainVerticalStack => idx != 0,
Self::HorizontalStack => idx != 0 && idx != count - 1,
Self::UltrawideVerticalStack => match count {
0 | 1 => false,
2 => idx != 0,
_ => idx < 2,
},

View File

@@ -2,12 +2,12 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use super::Arrangement;
use super::CustomLayout;
use super::DefaultLayout;
use super::Direction;
use crate::Arrangement;
use crate::CustomLayout;
use crate::DefaultLayout;
use crate::Direction;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum Layout {
Default(DefaultLayout),
Custom(CustomLayout),

View File

@@ -1,5 +1,5 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc, clippy::use_self)]
use std::path::Path;
use std::path::PathBuf;
@@ -14,7 +14,6 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
pub use animation::AnimationStyle;
pub use arrangement::Arrangement;
pub use arrangement::Axis;
pub use custom_layout::CustomLayout;
@@ -25,9 +24,7 @@ pub use layout::Layout;
pub use operation_direction::OperationDirection;
pub use rect::Rect;
pub mod animation;
pub mod arrangement;
pub mod asc;
pub mod config_generation;
pub mod custom_layout;
pub mod cycle_direction;
@@ -46,13 +43,10 @@ pub enum SocketMessage {
CycleFocusWindow(CycleDirection),
CycleMoveWindow(CycleDirection),
StackWindow(OperationDirection),
UnstackWindow,
CycleStack(CycleDirection),
FocusStackWindow(usize),
StackAll,
UnstackAll,
ResizeWindowEdge(OperationDirection, Sizing),
ResizeWindowAxis(Axis, Sizing),
UnstackWindow,
CycleStack(CycleDirection),
MoveContainerToMonitorNumber(usize),
CycleMoveContainerToMonitor(CycleDirection),
MoveContainerToWorkspaceNumber(usize),
@@ -78,7 +72,6 @@ pub enum SocketMessage {
ToggleMonocle,
ToggleMaximize,
ToggleWindowContainerBehaviour,
ToggleFloatOverride,
WindowHidingBehaviour(HidingBehaviour),
ToggleCrossMonitorMoveBehaviour,
CrossMonitorMoveBehaviour(MoveBehaviour),
@@ -92,8 +85,6 @@ pub enum SocketMessage {
CycleLayout(CycleDirection),
ChangeLayoutCustom(PathBuf),
FlipLayout(Axis),
ToggleWorkspaceWindowContainerBehaviour,
ToggleWorkspaceFloatOverride,
// Monitor and Workspace Commands
MonitorIndexPreference(usize, i32, i32, i32, i32),
DisplayIndexPreference(usize, String),
@@ -104,7 +95,6 @@ pub enum SocketMessage {
Stop,
TogglePause,
Retile,
RetileWithResizeDimensions,
QuickSave,
QuickLoad,
Save(PathBuf),
@@ -138,15 +128,10 @@ pub enum SocketMessage {
ClearNamedWorkspaceLayoutRules(String),
// Configuration
ReloadConfiguration,
ReplaceConfiguration(PathBuf),
ReloadStaticConfiguration(PathBuf),
WatchConfiguration(bool),
CompleteConfiguration,
AltFocusHack(bool),
Animation(bool),
AnimationDuration(u64),
AnimationFps(u64),
AnimationStyle(AnimationStyle),
#[serde(alias = "ActiveWindowBorder")]
Border(bool),
#[serde(alias = "ActiveWindowBorderColour")]
@@ -155,10 +140,6 @@ pub enum SocketMessage {
BorderStyle(BorderStyle),
BorderWidth(i32),
BorderOffset(i32),
BorderImplementation(BorderImplementation),
Transparency(bool),
ToggleTransparency,
TransparencyAlpha(u8),
InvisibleBorders(Rect),
StackbarMode(StackbarMode),
StackbarLabel(StackbarLabel),
@@ -167,8 +148,6 @@ pub enum SocketMessage {
StackbarBackgroundColour(u32, u32, u32),
StackbarHeight(i32),
StackbarTabWidth(i32),
StackbarFontSize(i32),
StackbarFontFamily(Option<String>),
WorkAreaOffset(Rect),
MonitorWorkAreaOffset(usize, Rect),
ResizeDelta(i32),
@@ -176,11 +155,7 @@ pub enum SocketMessage {
InitialNamedWorkspaceRule(ApplicationIdentifier, String, String),
WorkspaceRule(ApplicationIdentifier, String, usize, usize),
NamedWorkspaceRule(ApplicationIdentifier, String, String),
ClearWorkspaceRules(usize, usize),
ClearNamedWorkspaceRules(String),
ClearAllWorkspaceRules,
#[serde(alias = "FloatRule")]
IgnoreRule(ApplicationIdentifier, String),
FloatRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
IdentifyObjectNameChangeApplication(ApplicationIdentifier, String),
IdentifyTrayApplication(ApplicationIdentifier, String),
@@ -189,7 +164,6 @@ pub enum SocketMessage {
State,
GlobalState,
VisibleWindows,
MonitorInformation,
Query(StateQuery),
FocusFollowsMouse(FocusFollowsMouseImplementation, bool),
ToggleFocusFollowsMouse(FocusFollowsMouseImplementation),
@@ -198,7 +172,6 @@ pub enum SocketMessage {
RemoveTitleBar(ApplicationIdentifier, String),
ToggleTitleBars,
AddSubscriberSocket(String),
AddSubscriberSocketWithOptions(String, SubscribeOptions),
RemoveSubscriberSocket(String),
AddSubscriberPipe(String),
RemoveSubscriberPipe(String),
@@ -224,12 +197,6 @@ 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,
@@ -247,17 +214,7 @@ pub enum StackbarLabel {
}
#[derive(
Default,
Copy,
Clone,
Debug,
Eq,
PartialEq,
Display,
Serialize,
Deserialize,
JsonSchema,
ValueEnum,
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
)]
pub enum BorderStyle {
#[default]
@@ -270,44 +227,13 @@ pub enum BorderStyle {
}
#[derive(
Default,
Copy,
Clone,
Debug,
Eq,
PartialEq,
Display,
Serialize,
Deserialize,
JsonSchema,
ValueEnum,
)]
pub enum BorderImplementation {
#[default]
/// Use the adjustable komorebi border implementation
Komorebi,
/// Use the thin Windows accent border implementation
Windows,
}
#[derive(
Copy,
Clone,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum WindowKind {
Single,
Stack,
Monocle,
Unfocused,
Floating,
}
#[derive(
@@ -345,16 +271,7 @@ pub enum ApplicationIdentifier {
}
#[derive(
Copy,
Clone,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum FocusFollowsMouseImplementation {
/// A custom FFM implementation (slightly more CPU-intensive)
@@ -363,48 +280,18 @@ 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,
Default,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
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,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum MoveBehaviour {
/// Swap the window container with the window container at the edge of the adjacent monitor
@@ -415,16 +302,6 @@ pub enum MoveBehaviour {
NoOp,
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum CrossBoundaryBehaviour {
/// Attempt to perform actions across a workspace boundary
Workspace,
/// Attempt to perform actions across a monitor boundary
Monitor,
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
@@ -433,21 +310,12 @@ pub enum HidingBehaviour {
Hide,
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
Minimize,
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
Cloak,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
pub enum OperationBehaviour {
/// Process komorebic commands on temporarily unmanaged/floated windows

View File

@@ -7,8 +7,8 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use super::direction::Direction;
use super::Axis;
use crate::direction::Direction;
use crate::Axis;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,

View File

@@ -84,14 +84,4 @@ impl Rect {
bottom: (self.bottom * rect_dpi) / system_dpi,
}
}
#[must_use]
pub const fn rect(&self) -> RECT {
RECT {
left: self.left,
top: self.top,
right: self.left + self.right,
bottom: self.top + self.bottom,
}
}
}

Some files were not shown because too many files have changed in this diff Show More