mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-01-13 14:23:24 +01:00
Compare commits
168 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70d3e9448c | ||
|
|
9b0393e14b | ||
|
|
10af1f3c84 | ||
|
|
dcb1cd32fa | ||
|
|
17a8ac5810 | ||
|
|
8805fafa99 | ||
|
|
1219c3d118 | ||
|
|
b792328676 | ||
|
|
a7380db47c | ||
|
|
086993a7c0 | ||
|
|
51e1337f40 | ||
|
|
6cef8d9ef6 | ||
|
|
6e36b81669 | ||
|
|
0758c7d900 | ||
|
|
f77a303b30 | ||
|
|
3a81f7babb | ||
|
|
3d8778a7d6 | ||
|
|
a42e809ade | ||
|
|
66c5766848 | ||
|
|
ac56590791 | ||
|
|
4d67f5fed3 | ||
|
|
c91224295f | ||
|
|
4b1e3bd448 | ||
|
|
1cb8ed7f10 | ||
|
|
90271f1f99 | ||
|
|
9fde9ed6f8 | ||
|
|
ce4479eb9f | ||
|
|
f3b7f5ac42 | ||
|
|
382896dfd2 | ||
|
|
ff4187aecf | ||
|
|
8f982e45d1 | ||
|
|
309bfdfee1 | ||
|
|
a0239faea0 | ||
|
|
999dd5d20d | ||
|
|
69086299e1 | ||
|
|
4114b10b05 | ||
|
|
71402fe01b | ||
|
|
5c2767d589 | ||
|
|
ac67c41377 | ||
|
|
faed6ec535 | ||
|
|
4ebba08b7d | ||
|
|
a733fecb49 | ||
|
|
31c969fc55 | ||
|
|
df2adde13d | ||
|
|
a77b3e774a | ||
|
|
c8c4c3507c | ||
|
|
df38facf9e | ||
|
|
416dd94670 | ||
|
|
d0ae92ca3a | ||
|
|
3491dc7590 | ||
|
|
adbb6c1cb0 | ||
|
|
18ee667896 | ||
|
|
1a42c64620 | ||
|
|
b613986474 | ||
|
|
e93751aa5f | ||
|
|
9872dcd0d7 | ||
|
|
f99c82f5d4 | ||
|
|
b255058467 | ||
|
|
2abe618354 | ||
|
|
e953715fef | ||
|
|
2ac1929117 | ||
|
|
e33a5f28f0 | ||
|
|
15d069f2bf | ||
|
|
3e9947c2e2 | ||
|
|
1c23439f95 | ||
|
|
6eb2905e00 | ||
|
|
52a745d0a3 | ||
|
|
80877cc449 | ||
|
|
160cb7202d | ||
|
|
8d085df1ba | ||
|
|
5d48a5c5b4 | ||
|
|
539aeec965 | ||
|
|
bcfb058bc3 | ||
|
|
e07b464b0d | ||
|
|
ffa76ea28c | ||
|
|
2c00d79968 | ||
|
|
78177af6b8 | ||
|
|
86e0d40828 | ||
|
|
48f6ac8964 | ||
|
|
1db572f789 | ||
|
|
3dad77533b | ||
|
|
f40fb9a251 | ||
|
|
b4e16e43e9 | ||
|
|
4c2e8ff6d2 | ||
|
|
c879aae1e7 | ||
|
|
7e87e83189 | ||
|
|
aae9338f66 | ||
|
|
f68a709f1d | ||
|
|
c76846ac63 | ||
|
|
b29dd8b1d1 | ||
|
|
59c3c14731 | ||
|
|
db96f2cc5a | ||
|
|
a37a6752a8 | ||
|
|
4d0df9c5b5 | ||
|
|
f8ea62f857 | ||
|
|
93bb41737b | ||
|
|
280352eeef | ||
|
|
7619b9b4ed | ||
|
|
72a4d5276e | ||
|
|
4bb3b83d57 | ||
|
|
ccd2f3a464 | ||
|
|
7e242ada66 | ||
|
|
5b2acd0f12 | ||
|
|
ec0bbaae98 | ||
|
|
3c44f3bfdb | ||
|
|
7839980ddf | ||
|
|
6416c0b6eb | ||
|
|
f6ccec9505 | ||
|
|
21cb5e1e6f | ||
|
|
009c0dcd28 | ||
|
|
98c5ab3b9b | ||
|
|
d4eeec994f | ||
|
|
e9ed1cfd3b | ||
|
|
4a2eb391f7 | ||
|
|
41e18bccc6 | ||
|
|
3d373b3630 | ||
|
|
b4e61b079c | ||
|
|
eec6312a51 | ||
|
|
00384ce333 | ||
|
|
e75578d9ce | ||
|
|
50c850cb02 | ||
|
|
5a1af5b133 | ||
|
|
74c433185a | ||
|
|
71bb346c89 | ||
|
|
3feff1dca9 | ||
|
|
3019eaf89c | ||
|
|
ce59bd9ae4 | ||
|
|
309dd159ca | ||
|
|
6f1d6dbdc7 | ||
|
|
6a10d583a6 | ||
|
|
270ea5aa46 | ||
|
|
a9d2738733 | ||
|
|
3d8f68e559 | ||
|
|
80bb7288c4 | ||
|
|
76c833f661 | ||
|
|
80bce4be7e | ||
|
|
8c10547325 | ||
|
|
8f886b3fe4 | ||
|
|
53c38e157f | ||
|
|
70f561e6ac | ||
|
|
4ea835fa59 | ||
|
|
09137af305 | ||
|
|
ee89b344df | ||
|
|
46d5ea4a1d | ||
|
|
82c2241601 | ||
|
|
c28773b96a | ||
|
|
577364a556 | ||
|
|
17cd0308cb | ||
|
|
10424b696f | ||
|
|
6e7d8fb922 | ||
|
|
917cd9b7db | ||
|
|
bdbd665b21 | ||
|
|
f3f2098451 | ||
|
|
4ca2e8388b | ||
|
|
31752e422a | ||
|
|
5e308b9131 | ||
|
|
1bf53b89af | ||
|
|
11690c6004 | ||
|
|
3457dfc04c | ||
|
|
af1c9b5aa9 | ||
|
|
22fac5a9fb | ||
|
|
7a3990f106 | ||
|
|
74e93e5524 | ||
|
|
86e78570d6 | ||
|
|
3ee3aac806 | ||
|
|
c408c1149c | ||
|
|
a8b02f40fd | ||
|
|
6608e5a5bb |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -55,6 +55,7 @@ body:
|
||||
label: Hotkey Configuration
|
||||
description: >
|
||||
Please provide your whkdrc or komorebi.ahk hotkey configuration file
|
||||
render: shell
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
@@ -62,3 +63,4 @@ body:
|
||||
label: Output of komorebic check
|
||||
description: >
|
||||
Please provide the output of `komorebic check`
|
||||
render: shell
|
||||
|
||||
2
.github/workflows/feature-check.yaml
vendored
2
.github/workflows/feature-check.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check and close feature issues
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
23
.github/workflows/windows.yaml
vendored
23
.github/workflows/windows.yaml
vendored
@@ -13,15 +13,15 @@ on:
|
||||
- hotfix/*
|
||||
tags:
|
||||
- v*
|
||||
schedule:
|
||||
- cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
# schedule:
|
||||
# - cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
@@ -43,10 +43,11 @@ jobs:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: rustup toolchain install stable --profile minimal
|
||||
- run: rustup component add --toolchain stable-x86_64-pc-windows-msvc clippy
|
||||
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -64,7 +65,7 @@ jobs:
|
||||
- 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
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
|
||||
path: |
|
||||
@@ -81,12 +82,12 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v6
|
||||
- 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
|
||||
@@ -128,14 +129,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v6
|
||||
- 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
|
||||
@@ -170,14 +171,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v6
|
||||
- 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
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -6,3 +6,9 @@ dummy.go
|
||||
komorebic/applications.yaml
|
||||
komorebic/applications.json
|
||||
/.vs
|
||||
/bar-schema
|
||||
/komorebi-schema
|
||||
/.wrangler
|
||||
/.xwin-cache
|
||||
result
|
||||
/.direnv
|
||||
|
||||
3787
Cargo.lock
generated
3787
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
98
Cargo.toml
98
Cargo.toml
@@ -2,13 +2,14 @@
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"komorebi",
|
||||
"komorebi-client",
|
||||
"komorebi-gui",
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes"
|
||||
"komorebi",
|
||||
"komorebi-client",
|
||||
"komorebi-gui",
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes",
|
||||
"komorebi-shortcuts",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -18,12 +19,12 @@ chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.31"
|
||||
egui_extras = "0.31"
|
||||
eframe = "0.33"
|
||||
egui_extras = "0.33"
|
||||
dirs = "6"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
schemars = "0.8"
|
||||
schemars = "1.1"
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
||||
@@ -32,44 +33,55 @@ strum = { version = "0.27", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
parking_lot = "0.12"
|
||||
paste = "1"
|
||||
sysinfo = "0.33"
|
||||
sysinfo = "0.37"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "a28c6559a9de2f92c142a714947a9b081776caca" }
|
||||
windows-numerics = { version = "0.2" }
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
|
||||
windows-numerics = { version = "0.3" }
|
||||
windows-implement = { version = "0.60" }
|
||||
windows-interface = { version = "0.59" }
|
||||
windows-core = { version = "0.61" }
|
||||
windows-core = { version = "0.62" }
|
||||
shadow-rs = "1"
|
||||
which = "7"
|
||||
which = "8"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.61"
|
||||
version = "0.62"
|
||||
features = [
|
||||
"Foundation_Numerics",
|
||||
"Win32_Devices",
|
||||
"Win32_Devices_Display",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Power",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Accessibility",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control"
|
||||
]
|
||||
"Foundation_Numerics",
|
||||
"Win32_Devices",
|
||||
"Win32_Devices_Display",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Power",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Accessibility",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control",
|
||||
]
|
||||
|
||||
[profile.release-opt]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "komorebi-workspace"
|
||||
|
||||
42
README.md
42
README.md
@@ -29,6 +29,46 @@ Tiling Window Management for Windows.
|
||||
|
||||

|
||||
|
||||
## Note: Students using devices enrolled in mobile device management (MDM)
|
||||
|
||||
Your usage still falls under the [Komorebi License 2.0.0](./LICENSE.md).
|
||||
|
||||
You can email me at the address I sign my commits with (add `.patch` to the end
|
||||
of any commit URL on GitHub to find it) from the address associated with your
|
||||
institution with the subject "komorebi - student with an MDM device", and I will
|
||||
be able to remove the splash intended for corporate users, whose usage falls
|
||||
under the [Individual Commercial Use
|
||||
License](https://lgug2z.com/software/komorebi).
|
||||
|
||||
This is currently a manual process - most days this shouldn't take more than
|
||||
12h, and you will receive an email reply from me when the process is complete.
|
||||
|
||||
If you haven't had a reply to your email within 24h you can reach out to me on
|
||||
Discord.
|
||||
|
||||
## Note: Unexpected mobile device management (MDM) detection prompts
|
||||
|
||||
You have most likely unintentionally enrolled your device in "Bring Your Own
|
||||
Device" (BYOD) MDM. You can confirm if this is the case by running `dsregcmd
|
||||
/status` and then take the appropriate steps to remove the MDM profile and take
|
||||
back full control of your system.
|
||||
|
||||
If you need help doing this you can ask on Discord.
|
||||
|
||||
## Note: komorebi for Mac
|
||||
|
||||
If you made your way to this repo looking for [komorebi for
|
||||
Mac](https://github.com/KomoCorp/komorebi-for-mac), the project is currently
|
||||
being developed in private with [early access available to GitHub
|
||||
Sponsors](https://github.com/sponsors/LGUG2Z).
|
||||
|
||||
If you want to see how far along development is before signing up for early
|
||||
access (spoiler: it's very far along!) there is an overview video you can watch
|
||||
[here](https://www.youtube.com/watch?v=u3eJcsa_MJk).
|
||||
|
||||
Sponsors with early access can install komorebi for Mac either by compiling
|
||||
from source, by using Homebrew, or by using the project's Nix Flake.
|
||||
|
||||
## Overview
|
||||
|
||||
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
|
||||
@@ -394,7 +434,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.36"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.39"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
|
||||
432
check_schema_docs.py
Normal file
432
check_schema_docs.py
Normal file
@@ -0,0 +1,432 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check schema.json and schema.bar.json for missing docstrings and map them to Rust source files.
|
||||
|
||||
This script analyzes the generated JSON schemas and identifies:
|
||||
1. Type definitions ($defs) missing top-level descriptions
|
||||
2. Enum variants missing descriptions (in oneOf/anyOf)
|
||||
3. Enum variants missing titles (object variants in oneOf/anyOf)
|
||||
4. Struct properties missing descriptions
|
||||
5. Top-level schema properties missing descriptions
|
||||
|
||||
For each missing docstring, it attempts to find the corresponding Rust source
|
||||
file and line number where the docstring should be added.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class MissingDoc:
|
||||
type_name: str
|
||||
kind: str # "type", "variant", "property", "variant_title"
|
||||
item_name: Optional[str] # variant or property name
|
||||
rust_file: Optional[str] = None
|
||||
rust_line: Optional[int] = None
|
||||
|
||||
def __str__(self):
|
||||
location = ""
|
||||
if self.rust_file and self.rust_line:
|
||||
location = f" -> {self.rust_file}:{self.rust_line}"
|
||||
elif self.rust_file:
|
||||
location = f" -> {self.rust_file}"
|
||||
|
||||
if self.kind == "type":
|
||||
return f"[TYPE] {self.type_name}{location}"
|
||||
elif self.kind == "variant":
|
||||
return f"[VARIANT] {self.type_name}::{self.item_name}{location}"
|
||||
elif self.kind == "variant_title":
|
||||
return f"[VARIANT_TITLE] {self.type_name}::{self.item_name}{location}"
|
||||
else:
|
||||
return f"[PROPERTY] {self.type_name}.{self.item_name}{location}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SchemaConfig:
|
||||
"""Configuration for a schema to check."""
|
||||
|
||||
schema_file: str
|
||||
search_paths: list[str]
|
||||
display_name: str
|
||||
|
||||
|
||||
def find_rust_definition(
|
||||
type_name: str, item_name: Optional[str], kind: str, search_paths: list[Path]
|
||||
) -> tuple[Optional[str], Optional[int]]:
|
||||
"""Find the Rust file and line number for a type/variant/property definition."""
|
||||
|
||||
if kind == "type":
|
||||
patterns = [
|
||||
rf"pub\s+enum\s+{type_name}\b",
|
||||
rf"pub\s+struct\s+{type_name}\b",
|
||||
]
|
||||
elif kind in ("variant", "variant_title"):
|
||||
patterns = [
|
||||
rf"^\s*{re.escape(item_name)}\s*[,\(\{{]",
|
||||
rf"^\s*{re.escape(item_name)}\s*$",
|
||||
rf"^\s*#\[.*\]\s*\n\s*{re.escape(item_name)}\b",
|
||||
]
|
||||
else: # property
|
||||
patterns = [rf"pub\s+{re.escape(item_name)}\s*:"]
|
||||
|
||||
for search_path in search_paths:
|
||||
for rust_file in search_path.rglob("*.rs"):
|
||||
try:
|
||||
content = rust_file.read_text()
|
||||
lines = content.split("\n")
|
||||
|
||||
if kind == "type":
|
||||
for pattern in patterns:
|
||||
for i, line in enumerate(lines):
|
||||
if re.search(pattern, line):
|
||||
return str(rust_file), i + 1
|
||||
|
||||
elif kind in ("variant", "variant_title", "property"):
|
||||
parent_pattern = rf"pub\s+(?:enum|struct)\s+{type_name}\b"
|
||||
in_type = False
|
||||
brace_count = 0
|
||||
found_open_brace = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if re.search(parent_pattern, line):
|
||||
in_type = True
|
||||
brace_count = 0
|
||||
found_open_brace = False
|
||||
|
||||
if in_type:
|
||||
if "{" in line:
|
||||
found_open_brace = True
|
||||
brace_count += line.count("{") - line.count("}")
|
||||
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, line):
|
||||
return str(rust_file), i + 1
|
||||
|
||||
if found_open_brace and brace_count <= 0:
|
||||
in_type = False
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def _get_variant_identifier(variant: dict) -> str:
|
||||
"""Extract a meaningful identifier for a variant.
|
||||
|
||||
Tries to find the best identifier by checking:
|
||||
1. A top-level const value (e.g., {"const": "Linear"})
|
||||
2. A property with a const value (e.g., {"kind": {"const": "Bar"}})
|
||||
3. The first required property name
|
||||
4. The type field
|
||||
5. Falls back to "unknown"
|
||||
"""
|
||||
# Check for top-level const value (simple enum variant)
|
||||
if "const" in variant:
|
||||
return str(variant["const"])
|
||||
|
||||
properties = variant.get("properties", {})
|
||||
|
||||
# Check for a property with a const value (common pattern for tagged enums)
|
||||
for prop_name, prop_def in properties.items():
|
||||
if isinstance(prop_def, dict) and "const" in prop_def:
|
||||
return str(prop_def["const"])
|
||||
|
||||
# Fall back to first required property name
|
||||
required = variant.get("required", [])
|
||||
if required:
|
||||
return str(required[0])
|
||||
|
||||
# Fall back to type
|
||||
if "type" in variant:
|
||||
return str(variant["type"])
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def check_type_description(type_name: str, type_def: dict) -> list[MissingDoc]:
|
||||
"""Check if a type definition has proper documentation."""
|
||||
missing = []
|
||||
has_top_description = "description" in type_def
|
||||
|
||||
# Always check for top-level type description first
|
||||
# (except for types that are purely references or have special handling)
|
||||
needs_type_description = True
|
||||
|
||||
# Check oneOf variants (tagged enums with variant descriptions)
|
||||
if "oneOf" in type_def:
|
||||
# oneOf types should have a top-level description
|
||||
if not has_top_description:
|
||||
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||
|
||||
for variant in type_def["oneOf"]:
|
||||
# Case 1: Simple const variant (e.g., {"const": "Swap", "description": "..."})
|
||||
variant_name = variant.get("const") or variant.get("title")
|
||||
if variant_name and "description" not in variant:
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant", str(variant_name), None, None)
|
||||
)
|
||||
|
||||
# Case 2: String enum inside oneOf (e.g., {"type": "string", "enum": [...]})
|
||||
# These variants don't have individual descriptions in the schema
|
||||
if "enum" in variant and variant.get("type") == "string":
|
||||
for enum_variant in variant["enum"]:
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant", str(enum_variant), None, None)
|
||||
)
|
||||
|
||||
# Case 3: Object variant with properties (e.g., CubicBezier)
|
||||
if "properties" in variant and "description" not in variant:
|
||||
for prop_name in variant.get("required", []):
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant", str(prop_name), None, None)
|
||||
)
|
||||
|
||||
# Case 4: Object variant missing title (needed for schema UI display)
|
||||
# Object variants should have a title or const for proper display in editors
|
||||
if (
|
||||
"properties" in variant
|
||||
and "title" not in variant
|
||||
and "const" not in variant
|
||||
):
|
||||
# Try to find a good identifier for the variant (for display only)
|
||||
variant_id = _get_variant_identifier(variant)
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant_title", str(variant_id), None, None)
|
||||
)
|
||||
|
||||
# Check anyOf variants - check each variant individually
|
||||
elif "anyOf" in type_def:
|
||||
# anyOf types should have a top-level description
|
||||
if not has_top_description:
|
||||
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||
|
||||
# Check each variant for description (skip pure $ref and null types)
|
||||
for variant in type_def["anyOf"]:
|
||||
# Skip null type variants (used for Option<T>)
|
||||
if variant.get("type") == "null":
|
||||
continue
|
||||
# Skip pure $ref variants (the referenced type is checked separately)
|
||||
if "$ref" in variant and len(variant) == 1:
|
||||
continue
|
||||
# Skip $ref variants that have a description
|
||||
if "$ref" in variant and "description" in variant:
|
||||
continue
|
||||
# Variant with $ref but no description
|
||||
if "$ref" in variant and "description" not in variant:
|
||||
# Extract the type name from the $ref
|
||||
ref_name = variant["$ref"].split("/")[-1]
|
||||
missing.append(MissingDoc(type_name, "variant", ref_name, None, None))
|
||||
# Non-ref variant without description
|
||||
elif "description" not in variant and "$ref" not in variant:
|
||||
# Try to identify the variant by its type or const
|
||||
variant_id = variant.get("const") or variant.get("type") or "unknown"
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant", str(variant_id), None, None)
|
||||
)
|
||||
|
||||
# Check for missing title on object variants in anyOf
|
||||
if (
|
||||
"properties" in variant
|
||||
and "title" not in variant
|
||||
and "const" not in variant
|
||||
):
|
||||
variant_id = _get_variant_identifier(variant)
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant_title", str(variant_id), None, None)
|
||||
)
|
||||
|
||||
# Check simple string enums (no oneOf means no variant descriptions possible in schema)
|
||||
elif "enum" in type_def:
|
||||
if not has_top_description:
|
||||
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||
# Each enum variant needs a docstring - these can't have descriptions in simple enum format
|
||||
for variant in type_def["enum"]:
|
||||
missing.append(MissingDoc(type_name, "variant", str(variant), None, None))
|
||||
|
||||
# Check struct properties
|
||||
elif "properties" in type_def:
|
||||
# Structs should always have a top-level description
|
||||
if not has_top_description:
|
||||
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||
|
||||
for prop_name, prop_def in type_def["properties"].items():
|
||||
if "description" not in prop_def:
|
||||
missing.append(MissingDoc(type_name, "property", prop_name, None, None))
|
||||
|
||||
# Simple type without description (like PathBuf, Hex)
|
||||
elif not has_top_description:
|
||||
# Only flag if it has a concrete type (not just a $ref)
|
||||
if type_def.get("type") is not None:
|
||||
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||
|
||||
return missing
|
||||
|
||||
|
||||
def check_top_level_properties(schema: dict, root_type_name: str) -> list[MissingDoc]:
|
||||
"""Check top-level schema properties for missing descriptions."""
|
||||
missing = []
|
||||
properties = schema.get("properties", {})
|
||||
|
||||
for prop_name, prop_def in properties.items():
|
||||
if "description" not in prop_def:
|
||||
missing.append(
|
||||
MissingDoc(root_type_name, "property", prop_name, None, None)
|
||||
)
|
||||
|
||||
return missing
|
||||
|
||||
|
||||
def check_schema(
|
||||
schema_path: Path,
|
||||
search_paths: list[Path],
|
||||
project_root: Path,
|
||||
display_name: str,
|
||||
) -> tuple[list[MissingDoc], int]:
|
||||
"""Check a single schema file and return missing docs and exit code."""
|
||||
if not schema_path.exists():
|
||||
print(f"Error: {schema_path.name} not found at {schema_path}")
|
||||
return [], 1
|
||||
|
||||
with open(schema_path) as f:
|
||||
schema = json.load(f)
|
||||
|
||||
all_missing: list[MissingDoc] = []
|
||||
|
||||
# Check top-level schema properties
|
||||
root_type_name = schema.get("title", "Root")
|
||||
all_missing.extend(check_top_level_properties(schema, root_type_name))
|
||||
|
||||
# Check all type definitions
|
||||
for type_name, type_def in sorted(schema.get("$defs", {}).items()):
|
||||
# Skip PerAnimationPrefixConfig2/3 as they're generated variants
|
||||
if (
|
||||
type_name.startswith("PerAnimationPrefixConfig")
|
||||
and type_name != "PerAnimationPrefixConfig"
|
||||
):
|
||||
continue
|
||||
all_missing.extend(check_type_description(type_name, type_def))
|
||||
|
||||
# Find Rust source locations
|
||||
print(f"Scanning Rust source files for {display_name}...", file=sys.stderr)
|
||||
for doc in all_missing:
|
||||
doc.rust_file, doc.rust_line = find_rust_definition(
|
||||
doc.type_name, doc.item_name, doc.kind, search_paths
|
||||
)
|
||||
if doc.rust_file:
|
||||
try:
|
||||
doc.rust_file = str(Path(doc.rust_file).relative_to(project_root))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return all_missing, 0
|
||||
|
||||
|
||||
def print_results(all_missing: list[MissingDoc], display_name: str) -> None:
|
||||
"""Print the results for a schema check."""
|
||||
# Group by file
|
||||
by_file: dict[str, list[MissingDoc]] = {}
|
||||
external: list[MissingDoc] = []
|
||||
|
||||
for doc in all_missing:
|
||||
if doc.rust_file:
|
||||
by_file.setdefault(doc.rust_file, []).append(doc)
|
||||
else:
|
||||
external.append(doc)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 70)
|
||||
print(f"MISSING DOCSTRINGS IN SCHEMA ({display_name})")
|
||||
print("=" * 70)
|
||||
|
||||
type_count = sum(1 for d in all_missing if d.kind == "type")
|
||||
variant_count = sum(1 for d in all_missing if d.kind == "variant")
|
||||
variant_title_count = sum(1 for d in all_missing if d.kind == "variant_title")
|
||||
prop_count = sum(1 for d in all_missing if d.kind == "property")
|
||||
|
||||
print(f"\nTotal: {len(all_missing)} missing docstrings/titles")
|
||||
print(f" - {type_count} types")
|
||||
print(f" - {variant_count} variants")
|
||||
print(f" - {variant_title_count} variant titles")
|
||||
print(f" - {prop_count} properties")
|
||||
|
||||
# Print by file
|
||||
for rust_file in sorted(by_file.keys()):
|
||||
docs = sorted(by_file[rust_file], key=lambda d: d.rust_line or 0)
|
||||
print(f"\n{rust_file}:")
|
||||
print("-" * len(rust_file))
|
||||
for doc in docs:
|
||||
print(f" {doc}")
|
||||
|
||||
# Print external items (types not found in source)
|
||||
if external:
|
||||
print(f"\nExternal/Unknown location:")
|
||||
print("-" * 25)
|
||||
for doc in external:
|
||||
print(f" {doc}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
|
||||
|
||||
def main():
|
||||
project_root = Path.cwd()
|
||||
|
||||
# Define schemas to check with their respective search paths
|
||||
schemas = [
|
||||
SchemaConfig(
|
||||
schema_file="schema.json",
|
||||
search_paths=["komorebi/src", "komorebi-themes/src"],
|
||||
display_name="komorebi",
|
||||
),
|
||||
SchemaConfig(
|
||||
schema_file="schema.bar.json",
|
||||
search_paths=["komorebi-bar/src", "komorebi-themes/src"],
|
||||
display_name="komorebi-bar",
|
||||
),
|
||||
]
|
||||
|
||||
total_missing = 0
|
||||
has_errors = False
|
||||
|
||||
for schema_config in schemas:
|
||||
schema_path = project_root / schema_config.schema_file
|
||||
search_paths = [
|
||||
project_root / p
|
||||
for p in schema_config.search_paths
|
||||
if (project_root / p).exists()
|
||||
]
|
||||
|
||||
missing, error_code = check_schema(
|
||||
schema_path,
|
||||
search_paths,
|
||||
project_root,
|
||||
schema_config.display_name,
|
||||
)
|
||||
|
||||
if error_code != 0:
|
||||
has_errors = True
|
||||
continue
|
||||
|
||||
print_results(missing, schema_config.display_name)
|
||||
total_missing += len(missing)
|
||||
|
||||
# Print combined summary
|
||||
if len(schemas) > 1:
|
||||
print("\n" + "=" * 70)
|
||||
print("COMBINED SUMMARY")
|
||||
print("=" * 70)
|
||||
print(f"Total missing docstrings across all schemas: {total_missing}")
|
||||
print("=" * 70)
|
||||
|
||||
if has_errors:
|
||||
return 1
|
||||
|
||||
return 1 if total_missing > 0 else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
94
deny.toml
94
deny.toml
@@ -1,8 +1,8 @@
|
||||
[graph]
|
||||
targets = [
|
||||
"x86_64-pc-windows-msvc",
|
||||
"i686-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"i686-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc",
|
||||
]
|
||||
all-features = false
|
||||
no-default-features = false
|
||||
@@ -12,65 +12,82 @@ feature-depth = 1
|
||||
|
||||
[advisories]
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" }
|
||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
|
||||
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
allow = [
|
||||
"0BSD",
|
||||
"Apache-2.0",
|
||||
"Artistic-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"MPL-2.0",
|
||||
"OFL-1.1",
|
||||
"Ubuntu-font-1.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"LicenseRef-Komorebi-1.0"
|
||||
"0BSD",
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"Artistic-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"MPL-2.0",
|
||||
"OFL-1.1",
|
||||
"Ubuntu-font-1.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"LicenseRef-Komorebi-2.0",
|
||||
]
|
||||
confidence-threshold = 0.8
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-client"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebic"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebic-no-console"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-themes"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-gui"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-bar"
|
||||
expression = "LicenseRef-Komorebi-1.0"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-shortcuts"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "whkd-core"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "whkd-parser"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
@@ -78,6 +95,11 @@ crate = "base16-egui-themes"
|
||||
expression = "MIT"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "win32-display-data"
|
||||
expression = "0BSD"
|
||||
license-files = []
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
wildcards = "allow"
|
||||
@@ -90,10 +112,12 @@ unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = [
|
||||
"https://github.com/LGUG2Z/base16-egui-themes",
|
||||
"https://github.com/LGUG2Z/catppuccin-egui",
|
||||
"https://github.com/LGUG2Z/windows-icons",
|
||||
"https://github.com/LGUG2Z/win32-display-data",
|
||||
"https://github.com/LGUG2Z/flavours",
|
||||
"https://github.com/LGUG2Z/base16_color_scheme",
|
||||
"https://github.com/LGUG2Z/base16-egui-themes",
|
||||
"https://github.com/LGUG2Z/windows-icons",
|
||||
"https://github.com/LGUG2Z/win32-display-data",
|
||||
"https://github.com/LGUG2Z/flavours",
|
||||
"https://github.com/LGUG2Z/base16_color_scheme",
|
||||
"https://github.com/LGUG2Z/whkd",
|
||||
"https://github.com/LGUG2Z/catppuccin-egui",
|
||||
"https://github.com/amPerl/egui-phosphor",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,8 @@ Options:
|
||||
Desired ease function for animation
|
||||
|
||||
[default: linear]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint,
|
||||
ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo,
|
||||
ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
|
||||
-a, --animation-type <ANIMATION_TYPE>
|
||||
Animation type to apply the style to. If not specified, sets global style
|
||||
|
||||
12
docs/cli/cancel-preselect.md
Normal file
12
docs/cli/cancel-preselect.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# cancel-preselect
|
||||
|
||||
```
|
||||
Cancel a workspace preselect set by the preselect-direction command, if one exists
|
||||
|
||||
Usage: komorebic.exe cancel-preselect
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
|
||||
|
||||
Arguments:
|
||||
<DEFAULT_LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -12,9 +12,6 @@ Options:
|
||||
--whkd
|
||||
Enable autostart of whkd
|
||||
|
||||
--ahk
|
||||
Enable autostart of ahk
|
||||
|
||||
--bar
|
||||
Enable autostart of komorebi-bar
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@ Options:
|
||||
--whkd
|
||||
Kill whkd if it is running as a background process
|
||||
|
||||
--ahk
|
||||
Kill ahk if it is running as a background process
|
||||
|
||||
--bar
|
||||
Kill komorebi-bar if it is running as a background process
|
||||
|
||||
|
||||
16
docs/cli/license.md
Normal file
16
docs/cli/license.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# license
|
||||
|
||||
```
|
||||
Specify an email associated with an Individual Commercial Use License
|
||||
|
||||
Usage: komorebic.exe license <EMAIL>
|
||||
|
||||
Arguments:
|
||||
<EMAIL>
|
||||
Email address associated with an Individual Commercial Use License
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -10,7 +10,7 @@ Arguments:
|
||||
Target workspace name
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
16
docs/cli/preselect-direction.md
Normal file
16
docs/cli/preselect-direction.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# preselect-direction
|
||||
|
||||
```
|
||||
Preselect the specified direction for the next window to be spawned on supported layouts
|
||||
|
||||
Usage: komorebic.exe preselect-direction <OPERATION_DIRECTION>
|
||||
|
||||
Arguments:
|
||||
<OPERATION_DIRECTION>
|
||||
[possible values: left, right, up, down]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/promote-swap.md
Normal file
12
docs/cli/promote-swap.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# promote-swap
|
||||
|
||||
```
|
||||
Promote the focused window to the largest tile by swapping container indices with the largest tile
|
||||
|
||||
Usage: komorebic.exe promote-swap
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# promote
|
||||
|
||||
```
|
||||
Promote the focused window to the top of the tree
|
||||
Promote the focused window to the largest tile via container removal and re-insertion
|
||||
|
||||
Usage: komorebic.exe promote
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage: komorebic.exe query <STATE_QUERY>
|
||||
|
||||
Arguments:
|
||||
<STATE_QUERY>
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, version]
|
||||
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, focused-container-kind, version]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
16
docs/cli/scrolling-layout-columns.md
Normal file
16
docs/cli/scrolling-layout-columns.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# scrolling-layout-columns
|
||||
|
||||
```
|
||||
Set the number of visible columns for the Scrolling layout on the focused workspace
|
||||
|
||||
Usage: komorebic.exe scrolling-layout-columns <COUNT>
|
||||
|
||||
Arguments:
|
||||
<COUNT>
|
||||
Desired number of visible columns
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -18,9 +18,6 @@ Options:
|
||||
--whkd
|
||||
Start whkd in a background process
|
||||
|
||||
--ahk
|
||||
Start autohotkey configuration file
|
||||
|
||||
--bar
|
||||
Start komorebi-bar in a background process
|
||||
|
||||
|
||||
@@ -9,9 +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
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# toggle-pause
|
||||
|
||||
```
|
||||
Toggle window tiling on the focused workspace
|
||||
Toggle the paused state for all window tiling
|
||||
|
||||
Usage: komorebic.exe toggle-pause
|
||||
|
||||
|
||||
12
docs/cli/toggle-shortcuts.md
Normal file
12
docs/cli/toggle-shortcuts.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# toggle-shortcuts
|
||||
|
||||
```
|
||||
Toggle the komorebi-shortcuts helper
|
||||
|
||||
Usage: komorebic.exe toggle-shortcuts
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -8,7 +8,7 @@ Usage: komorebic.exe window-hiding-behaviour <HIDING_BEHAVIOUR>
|
||||
Arguments:
|
||||
<HIDING_BEHAVIOUR>
|
||||
Possible values:
|
||||
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
- hide: END OF LIFE FEATURE: 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
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
31
docs/cli/workspace-work-area-offset.md
Normal file
31
docs/cli/workspace-work-area-offset.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# workspace-work-area-offset
|
||||
|
||||
```
|
||||
Set offsets for a workspace to exclude parts of the work area from tiling
|
||||
|
||||
Usage: komorebic.exe workspace-work-area-offset <MONITOR> <WORKSPACE> <LEFT> <TOP> <RIGHT> <BOTTOM>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index (zero-indexed)
|
||||
|
||||
<LEFT>
|
||||
Size of the left work area offset (set right to left * 2 to maintain right padding)
|
||||
|
||||
<TOP>
|
||||
Size of the top work area offset (set bottom to the same value to maintain bottom padding)
|
||||
|
||||
<RIGHT>
|
||||
Size of the right work area offset
|
||||
|
||||
<BOTTOM>
|
||||
Size of the bottom work area offset
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -6,7 +6,10 @@ defined in the `komorebi.json` configuration file.
|
||||
```json
|
||||
{
|
||||
"animation": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"duration": 250,
|
||||
"fps": 60,
|
||||
"style": "EaseOutSine"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -301,7 +301,7 @@ how to map the indices and would use default behaviour which would result in a m
|
||||
}
|
||||
```
|
||||
|
||||
# Multiple Monitors on different machines
|
||||
# Multiple monitors on different machines
|
||||
|
||||
You can use the same `komorebi.json` to configure two different setups and then synchronize your config across machines.
|
||||
However, if you do this it is important to be aware of a few things.
|
||||
@@ -393,6 +393,13 @@ This is because komorebi will apply the appropriate config to the loaded monitor
|
||||
index (the index defined in the user config) to the actual monitor index, and the bar will use that map to know if it
|
||||
should be enabled, and where it should be drawn.
|
||||
|
||||
# Windows Display Settings
|
||||
|
||||
In `Settings > System > Display > Multiple Displays`:
|
||||
|
||||
- Disable "Remember windows locations on monitor connection"
|
||||
- Enable "Minimize windows when a monitor is disconnected"
|
||||
|
||||
### Things to keep in mind
|
||||
|
||||
* If you are using a laptop connected to one monitor at work and a different one at home, the work monitor and the home
|
||||
|
||||
@@ -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>"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -16,6 +16,19 @@ the example files have been downloaded. For most new users this will be in the
|
||||
komorebic quickstart
|
||||
```
|
||||
|
||||
## Corporate Devices Enrolled in MDM
|
||||
|
||||
If you are using `komorebi` on a corporate device enrolled in mobile device
|
||||
management, you will receive a pop-up when you run `komorebic start` reminding
|
||||
you that the [Komorebi License](https://github.com/LGUG2Z/komorebi-license) does
|
||||
not permit any kind of commercial use.
|
||||
|
||||
You can remove this pop-up by running `komorebic license <email>` with the email
|
||||
associated with your Individual Commercial Use License. A single HTTP request
|
||||
will be sent with the given email address to verify license validity.
|
||||
|
||||
## Starting komorebi
|
||||
|
||||
With the example configurations downloaded, you can now start `komorebi`,
|
||||
`komorebi-bar` and `whkd`.
|
||||
|
||||
@@ -23,6 +36,9 @@ With the example configurations downloaded, you can now start `komorebi`,
|
||||
komorebic start --whkd --bar
|
||||
```
|
||||
|
||||
If you don't want to use the komorebi status bar, you can remove the `--bar` option
|
||||
from the above command.
|
||||
|
||||
## komorebi.json
|
||||
|
||||
The example window manager configuration sets some sane defaults and provides
|
||||
@@ -186,6 +202,9 @@ limitations on hotkey bindings that include the `win` key. However, you will sti
|
||||
to [modify the registry](https://superuser.com/questions/1059511/how-to-disable-winl-in-windows-10) to prevent
|
||||
`win + l` from locking the operating system.
|
||||
|
||||
You can toggle an overlay of the current `whkdrc` shortcuts related to `komorebi` at any time when using the example
|
||||
configuration with `alt + i`.
|
||||
|
||||
```
|
||||
{% include "./whkdrc.sample" %}
|
||||
```
|
||||
|
||||
@@ -120,6 +120,7 @@ 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
|
||||
cargo +stable install --path komorebi-shortcuts --locked
|
||||
```
|
||||
|
||||
If the binaries have been built and added to your `$PATH` correctly, you should
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.36/schema.bar.json",
|
||||
"monitor": 0,
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.bar.json",
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
"palette": "Base16",
|
||||
@@ -48,8 +47,8 @@
|
||||
{
|
||||
"Network": {
|
||||
"enable": true,
|
||||
"show_total_data_transmitted": true,
|
||||
"show_network_activity": true
|
||||
"show_activity": true,
|
||||
"show_total_activity": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.36/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
|
||||
@@ -138,13 +138,14 @@ running `komorebic stop` and `komorebic start`.
|
||||
Users with Nvidia GPUs may have issues with transparency on the Komorebi Bar.
|
||||
|
||||
To solve this the user can do the following:
|
||||
1. Open the Nvidia Control Panel
|
||||
2. On the left menu tree, under "3D Settings", select "Manage 3D Settings"
|
||||
3. Select the "Program Settings" tab
|
||||
4. Press the "Add" button and select "komorebi-bar"
|
||||
5. Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
|
||||
6. Change the setting to "Prefer compatibility"
|
||||
7. At the bottom of the window select "Apply"
|
||||
8. Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
|
||||
|
||||
- Open the Nvidia Control Panel
|
||||
- On the left menu tree, under "3D Settings", select "Manage 3D Settings"
|
||||
- Select the "Program Settings" tab
|
||||
- Press the "Add" button and select "komorebi-bar"
|
||||
- Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
|
||||
- Change the setting to "Prefer compatibility"
|
||||
- At the bottom of the window select "Apply"
|
||||
- Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
|
||||
|
||||
This should resolve the issue and your Komorebi Bar should render with the proper transparency.
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
alt + o : taskkill /f /im whkd.exe; Start-Process whkd -WindowStyle hidden # if shell is pwsh / powershell
|
||||
alt + shift + o : komorebic reload-configuration
|
||||
|
||||
alt + i : komorebic toggle-shortcuts
|
||||
|
||||
# App shortcuts - these require shell to be pwsh / powershell
|
||||
# The apps will be focused if open, or launched if not open
|
||||
# alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox }
|
||||
|
||||
179
flake.lock
generated
Normal file
179
flake.lock
generated
Normal file
@@ -0,0 +1,179 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1766774972,
|
||||
"narHash": "sha256-8qxEFpj4dVmIuPn9j9z6NTbU+hrcGjBOvaxTzre5HmM=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "01bc1d404a51a0a07e9d8759cd50a7903e218c82",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1761588595,
|
||||
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765835352,
|
||||
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks-nix": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765911976,
|
||||
"narHash": "sha256-t3T/xm8zstHRLx+pIHxVpQTiySbKqcQbK+r+01XVKc0=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"git-hooks-nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766870016,
|
||||
"narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1765674936,
|
||||
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-parts": "flake-parts",
|
||||
"git-hooks-nix": "git-hooks-nix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1766890375,
|
||||
"narHash": "sha256-0Zi7ChAtjq/efwQYmp7kOJPcSt6ya9ynSUe6ppgZhsQ=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "91e1f7a0017065360f447622d11b7ce6ed04772f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1766000401,
|
||||
"narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
360
flake.nix
Normal file
360
flake.nix
Normal file
@@ -0,0 +1,360 @@
|
||||
{
|
||||
description = "komorebi for Windows";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
crane.url = "github:ipetkov/crane";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
git-hooks-nix.url = "github:cachix/git-hooks.nix";
|
||||
git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-parts,
|
||||
crane,
|
||||
rust-overlay,
|
||||
...
|
||||
}:
|
||||
let
|
||||
windowsSdkVersion = "10.0.26100";
|
||||
windowsCrtVersion = "14.44.17.14";
|
||||
|
||||
mkWindowsSdk =
|
||||
pkgs:
|
||||
pkgs.stdenvNoCC.mkDerivation {
|
||||
name = "windows-sdk-${windowsSdkVersion}-crt-${windowsCrtVersion}";
|
||||
|
||||
nativeBuildInputs = [ pkgs.xwin ];
|
||||
|
||||
outputHashAlgo = "sha256";
|
||||
outputHashMode = "recursive";
|
||||
outputHash = "sha256-6cLS5q1BDRpLPScfmmKpTTEHUzsgKTKD1+mKvGX9Deo=";
|
||||
|
||||
buildCommand = ''
|
||||
export HOME=$(mktemp -d)
|
||||
xwin --accept-license \
|
||||
--sdk-version ${windowsSdkVersion} \
|
||||
--crt-version ${windowsCrtVersion} \
|
||||
splat --output $out
|
||||
'';
|
||||
};
|
||||
|
||||
mkMsvcEnv =
|
||||
{ pkgs, windowsSdk }:
|
||||
let
|
||||
clangVersion = pkgs.lib.versions.major pkgs.llvmPackages.clang.version;
|
||||
in
|
||||
{
|
||||
# linker for the windows target
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER = "lld-link";
|
||||
|
||||
# c/c++ compiler
|
||||
CC_x86_64_pc_windows_msvc = "clang-cl";
|
||||
CXX_x86_64_pc_windows_msvc = "clang-cl";
|
||||
AR_x86_64_pc_windows_msvc = "llvm-lib";
|
||||
|
||||
# IMPORTANT: libclang include path MUST come first to avoid header conflicts
|
||||
CFLAGS_x86_64_pc_windows_msvc = builtins.concatStringsSep " " [
|
||||
"--target=x86_64-pc-windows-msvc"
|
||||
"-Wno-unused-command-line-argument"
|
||||
"-fuse-ld=lld-link"
|
||||
"/imsvc${pkgs.llvmPackages.libclang.lib}/lib/clang/${clangVersion}/include"
|
||||
"/imsvc${windowsSdk}/crt/include"
|
||||
"/imsvc${windowsSdk}/sdk/include/ucrt"
|
||||
"/imsvc${windowsSdk}/sdk/include/um"
|
||||
"/imsvc${windowsSdk}/sdk/include/shared"
|
||||
];
|
||||
|
||||
CXXFLAGS_x86_64_pc_windows_msvc = builtins.concatStringsSep " " [
|
||||
"--target=x86_64-pc-windows-msvc"
|
||||
"-Wno-unused-command-line-argument"
|
||||
"-fuse-ld=lld-link"
|
||||
"/imsvc${pkgs.llvmPackages.libclang.lib}/lib/clang/${clangVersion}/include"
|
||||
"/imsvc${windowsSdk}/crt/include"
|
||||
"/imsvc${windowsSdk}/sdk/include/ucrt"
|
||||
"/imsvc${windowsSdk}/sdk/include/um"
|
||||
"/imsvc${windowsSdk}/sdk/include/shared"
|
||||
];
|
||||
|
||||
# target-specific rust flags with linker flavor and library search paths
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = builtins.concatStringsSep " " [
|
||||
"-Clinker-flavor=lld-link"
|
||||
"-Lnative=${windowsSdk}/crt/lib/x86_64"
|
||||
"-Lnative=${windowsSdk}/sdk/lib/um/x86_64"
|
||||
"-Lnative=${windowsSdk}/sdk/lib/ucrt/x86_64"
|
||||
];
|
||||
|
||||
# cargo target
|
||||
CARGO_BUILD_TARGET = "x86_64-pc-windows-msvc";
|
||||
};
|
||||
|
||||
mkKomorebiPackages =
|
||||
{ pkgs, windowsSdk }:
|
||||
let
|
||||
# toolchain with windows msvc target
|
||||
toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override {
|
||||
targets = [ "x86_64-pc-windows-msvc" ];
|
||||
};
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
|
||||
version = "0.1.0";
|
||||
|
||||
msvcEnv = mkMsvcEnv { inherit pkgs windowsSdk; };
|
||||
|
||||
src = pkgs.lib.cleanSourceWith {
|
||||
src = ./.;
|
||||
filter =
|
||||
path: type:
|
||||
(craneLib.filterCargoSources path type)
|
||||
|| (pkgs.lib.hasInfix "/docs/" path)
|
||||
|| (builtins.match ".*/docs/.*" path != null);
|
||||
};
|
||||
|
||||
commonArgs = {
|
||||
inherit src version;
|
||||
strictDeps = true;
|
||||
COMMIT_HASH = self.rev or (pkgs.lib.removeSuffix "-dirty" self.dirtyRev);
|
||||
|
||||
# build inputs for cross-compilation
|
||||
nativeBuildInputs = [
|
||||
pkgs.llvmPackages.clang-unwrapped
|
||||
pkgs.llvmPackages.lld
|
||||
pkgs.llvmPackages.llvm
|
||||
];
|
||||
|
||||
# cross-compilation environment
|
||||
inherit (msvcEnv)
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER
|
||||
CC_x86_64_pc_windows_msvc
|
||||
CXX_x86_64_pc_windows_msvc
|
||||
AR_x86_64_pc_windows_msvc
|
||||
CFLAGS_x86_64_pc_windows_msvc
|
||||
CXXFLAGS_x86_64_pc_windows_msvc
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS
|
||||
CARGO_BUILD_TARGET
|
||||
;
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
individualCrateArgs = commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
doCheck = false;
|
||||
doDoc = false;
|
||||
};
|
||||
|
||||
fullBuild = craneLib.buildPackage (
|
||||
individualCrateArgs
|
||||
// {
|
||||
pname = "komorebi-workspace";
|
||||
}
|
||||
);
|
||||
|
||||
extractBinary =
|
||||
binaryName:
|
||||
pkgs.runCommand "komorebi-${binaryName}"
|
||||
{
|
||||
meta = fullBuild.meta // { };
|
||||
}
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
cp ${fullBuild}/bin/${binaryName}.exe $out/bin/
|
||||
'';
|
||||
in
|
||||
{
|
||||
inherit
|
||||
craneLib
|
||||
src
|
||||
individualCrateArgs
|
||||
fullBuild
|
||||
msvcEnv
|
||||
;
|
||||
komorebi = extractBinary "komorebi";
|
||||
komorebic = extractBinary "komorebic";
|
||||
komorebic-no-console = extractBinary "komorebic-no-console";
|
||||
komorebi-bar = extractBinary "komorebi-bar";
|
||||
komorebi-gui = extractBinary "komorebi-gui";
|
||||
komorebi-shortcuts = extractBinary "komorebi-shortcuts";
|
||||
};
|
||||
|
||||
mkPkgs =
|
||||
system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
in
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
systems = [
|
||||
"aarch64-darwin"
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
|
||||
imports = [
|
||||
inputs.treefmt-nix.flakeModule
|
||||
inputs.git-hooks-nix.flakeModule
|
||||
];
|
||||
|
||||
perSystem =
|
||||
{ config, system, ... }:
|
||||
let
|
||||
pkgs = mkPkgs system;
|
||||
windowsSdk = mkWindowsSdk pkgs;
|
||||
build = mkKomorebiPackages { inherit pkgs windowsSdk; };
|
||||
|
||||
# toolchain with windows target and nightly rustfmt
|
||||
rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override {
|
||||
targets = [ "x86_64-pc-windows-msvc" ];
|
||||
};
|
||||
nightlyRustfmt = pkgs.rust-bin.nightly.latest.rustfmt;
|
||||
rustToolchainWithNightlyRustfmt = pkgs.symlinkJoin {
|
||||
name = "rust-toolchain-with-nightly-rustfmt";
|
||||
paths = [
|
||||
nightlyRustfmt
|
||||
rustToolchain
|
||||
];
|
||||
};
|
||||
nightlyToolchain = pkgs.rust-bin.nightly.latest.default.override {
|
||||
targets = [ "x86_64-pc-windows-msvc" ];
|
||||
};
|
||||
cargo-udeps = pkgs.writeShellScriptBin "cargo-udeps" ''
|
||||
export PATH="${nightlyToolchain}/bin:$PATH"
|
||||
exec ${pkgs.cargo-udeps}/bin/cargo-udeps "$@"
|
||||
'';
|
||||
in
|
||||
{
|
||||
treefmt = {
|
||||
projectRootFile = "flake.nix";
|
||||
programs = {
|
||||
deadnix.enable = true;
|
||||
just.enable = true;
|
||||
nixfmt.enable = true;
|
||||
taplo.enable = true;
|
||||
rustfmt = {
|
||||
enable = true;
|
||||
package = pkgs.rust-bin.nightly.latest.rustfmt;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
checks = {
|
||||
komorebi-workspace-clippy = build.craneLib.cargoClippy (
|
||||
build.individualCrateArgs
|
||||
// {
|
||||
cargoClippyExtraArgs = "--all-targets -- -D warnings";
|
||||
}
|
||||
);
|
||||
|
||||
komorebi-workspace-fmt = build.craneLib.cargoFmt {
|
||||
inherit (build) src;
|
||||
};
|
||||
|
||||
komorebi-workspace-toml-fmt = build.craneLib.taploFmt {
|
||||
src = pkgs.lib.sources.sourceFilesBySuffices build.src [ ".toml" ];
|
||||
};
|
||||
|
||||
komorebi-workspace-deny = build.craneLib.cargoDeny {
|
||||
inherit (build) src;
|
||||
};
|
||||
|
||||
komorebi-workspace-nextest = build.craneLib.cargoNextest build.individualCrateArgs;
|
||||
};
|
||||
|
||||
packages = {
|
||||
inherit (build)
|
||||
komorebi
|
||||
komorebic
|
||||
komorebic-no-console
|
||||
komorebi-bar
|
||||
komorebi-gui
|
||||
komorebi-shortcuts
|
||||
;
|
||||
inherit windowsSdk;
|
||||
komorebi-full = build.fullBuild;
|
||||
default = build.fullBuild;
|
||||
};
|
||||
|
||||
apps = {
|
||||
komorebi = {
|
||||
type = "app";
|
||||
program = "${build.komorebi}/bin/komorebi.exe";
|
||||
};
|
||||
komorebic = {
|
||||
type = "app";
|
||||
program = "${build.komorebic}/bin/komorebic.exe";
|
||||
};
|
||||
komorebic-no-console = {
|
||||
type = "app";
|
||||
program = "${build.komorebic-no-console}/bin/komorebic-no-console.exe";
|
||||
};
|
||||
komorebi-bar = {
|
||||
type = "app";
|
||||
program = "${build.komorebi-bar}/bin/komorebi-bar.exe";
|
||||
};
|
||||
komorebi-gui = {
|
||||
type = "app";
|
||||
program = "${build.komorebi-gui}/bin/komorebi-gui.exe";
|
||||
};
|
||||
komorebi-shortcuts = {
|
||||
type = "app";
|
||||
program = "${build.komorebi-shortcuts}/bin/komorebi-shortcuts.exe";
|
||||
};
|
||||
default = {
|
||||
type = "app";
|
||||
program = "${build.fullBuild}/bin/komorebi.exe";
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
name = "komorebi";
|
||||
|
||||
RUST_BACKTRACE = "full";
|
||||
|
||||
# cross-compilation environment
|
||||
inherit (build.msvcEnv)
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER
|
||||
CC_x86_64_pc_windows_msvc
|
||||
CXX_x86_64_pc_windows_msvc
|
||||
AR_x86_64_pc_windows_msvc
|
||||
CFLAGS_x86_64_pc_windows_msvc
|
||||
CXXFLAGS_x86_64_pc_windows_msvc
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS
|
||||
CARGO_BUILD_TARGET
|
||||
;
|
||||
|
||||
packages = [
|
||||
rustToolchainWithNightlyRustfmt
|
||||
cargo-udeps
|
||||
|
||||
# cross-compilation tooling
|
||||
pkgs.llvmPackages.clang-unwrapped # provides clang-cl
|
||||
pkgs.llvmPackages.lld # provides lld-link
|
||||
pkgs.llvmPackages.llvm # provides llvm-lib
|
||||
|
||||
pkgs.cargo-deny
|
||||
pkgs.cargo-nextest
|
||||
pkgs.cargo-outdated
|
||||
pkgs.jq
|
||||
pkgs.just
|
||||
pkgs.prettier
|
||||
];
|
||||
};
|
||||
|
||||
pre-commit = {
|
||||
check.enable = true;
|
||||
settings.hooks.treefmt = {
|
||||
enable = true;
|
||||
package = config.treefmt.build.wrapper;
|
||||
pass_filenames = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
50
justfile
50
justfile
@@ -15,6 +15,9 @@ fmt:
|
||||
prettier --write .github/FUNDING.yml
|
||||
prettier --write .github/workflows/windows.yaml
|
||||
|
||||
fix:
|
||||
cargo clippy --fix --allow-dirty
|
||||
|
||||
install-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
|
||||
|
||||
@@ -28,10 +31,10 @@ install-target-with-jsonschema target:
|
||||
cargo +stable install --path {{ target }} --locked
|
||||
|
||||
install:
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
|
||||
install-with-jsonschema:
|
||||
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
|
||||
build-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just build-target $_ }
|
||||
@@ -40,7 +43,7 @@ build-target target:
|
||||
cargo +stable build --package {{ target }} --locked --release --no-default-features
|
||||
|
||||
build:
|
||||
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
|
||||
copy-target target:
|
||||
cp .\target\release\{{ target }}.exe $Env:USERPROFILE\.cargo\bin
|
||||
@@ -52,7 +55,7 @@ wpm target:
|
||||
just build-target {{ target }} && wpmctl stop {{ target }}; just copy-target {{ target }} && wpmctl start {{ target }}
|
||||
|
||||
copy:
|
||||
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
|
||||
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts
|
||||
|
||||
run target:
|
||||
cargo +stable run --bin {{ target }} --locked --no-default-features
|
||||
@@ -72,19 +75,38 @@ trace target $RUST_LOG="trace":
|
||||
deadlock $RUST_LOG="trace":
|
||||
cargo +stable run --bin komorebi --locked --no-default-features --features deadlock_detection
|
||||
|
||||
docgen:
|
||||
cargo run --package komorebic -- docgen
|
||||
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
|
||||
docgen starlight:
|
||||
rm {{ starlight }}/src/data/cli/windows/*.md
|
||||
cargo run --package komorebic -- docgen --output {{ starlight }}/src/data/cli/windows
|
||||
schemars-docgen ./schema.json --output {{ starlight }}/src/content/docs/reference/komorebi-windows.mdx --title "komorebi.json (Windows)" --description "komorebi for Windows configuration schema reference"
|
||||
schemars-docgen ./schema.bar.json --output {{ starlight }}/src/content/docs/reference/bar-windows.mdx --title "komorebi.bar.json (Windows)" --description "komorebi-bar for Windows configuration schema reference"
|
||||
|
||||
jsonschema:
|
||||
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
|
||||
|
||||
# this part is run in a nix shell because python is a nightmare
|
||||
schemagen:
|
||||
rm -rf static-config-docs bar-config-docs
|
||||
mkdir -p static-config-docs bar-config-docs
|
||||
generate-schema-doc ./schema.json --config template_name=js_offline --config minify=false ./static-config-docs/
|
||||
generate-schema-doc ./schema.bar.json --config template_name=js_offline --config minify=false ./bar-config-docs/
|
||||
mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html
|
||||
version := `cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "komorebi") | .version'`
|
||||
|
||||
schemapub:
|
||||
rm -Force komorebi-schema
|
||||
mkdir -Force komorebi-schema
|
||||
cp schema.json komorebi-schema/komorebi.{{ version }}.schema.json
|
||||
cp schema.bar.json komorebi-schema/komorebi.bar.{{ version }}.schema.json
|
||||
npx wrangler pages deploy --project-name komorebi --branch main .\komorebi-schema
|
||||
|
||||
depcheck:
|
||||
cargo outdated --depth 2
|
||||
cargo +nightly udeps --quiet
|
||||
|
||||
deps:
|
||||
cargo update
|
||||
just depgen
|
||||
|
||||
depgen:
|
||||
cargo deny check
|
||||
cargo deny list --format json | jq 'del(.unlicensed)' > dependencies.json
|
||||
|
||||
procdump:
|
||||
cargo build --bin komorebi
|
||||
.\procdump.exe -ma -e -x . .\target\debug\komorebi.exe
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
# 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" }
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
komorebi-themes = { path = "../komorebi-themes", default-features = false }
|
||||
|
||||
chrono-tz = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
@@ -17,15 +17,16 @@ crossbeam-channel = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
egui-phosphor = "0.9"
|
||||
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" }
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
lazy_static = { workspace = true }
|
||||
netdev = "0.33"
|
||||
netdev = "0.40"
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
parking_lot = { workspace = true }
|
||||
random_word = { version = "0.5", features = ["en"] }
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
schemars = { workspace = true, optional = true }
|
||||
@@ -35,6 +36,7 @@ starship-battery = "0.10"
|
||||
sysinfo = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
which = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" }
|
||||
@@ -42,4 +44,8 @@ windows-icons-fallback = { package = "windows-icons", git = "https://github.com/
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["dep:schemars"]
|
||||
schemars = [
|
||||
"dep:schemars",
|
||||
"komorebi-client/default",
|
||||
"komorebi-themes/default",
|
||||
]
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
use crate::config::get_individual_spacing;
|
||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||
use crate::BAR_HEIGHT;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use crate::KomorebiEvent;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_LEFT;
|
||||
use crate::MONITOR_RIGHT;
|
||||
use crate::MONITOR_TOP;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::KomobarTheme;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
use crate::config::Position;
|
||||
use crate::config::PositionConfig;
|
||||
use crate::config::get_individual_spacing;
|
||||
use crate::process_hwnd;
|
||||
use crate::render::Color32Ext;
|
||||
use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::render::RenderExt;
|
||||
use crate::widgets::komorebi::Komorebi;
|
||||
use crate::widgets::komorebi::KomorebiNotificationState;
|
||||
use crate::widgets::komorebi::MonitorInfo;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::widgets::widget::WidgetConfig;
|
||||
use crate::KomorebiEvent;
|
||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||
use crate::BAR_HEIGHT;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::MONITOR_LEFT;
|
||||
use crate::MONITOR_RIGHT;
|
||||
use crate::MONITOR_TOP;
|
||||
use color_eyre::eyre;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use eframe::egui::Align;
|
||||
@@ -38,6 +39,7 @@ use eframe::egui::Frame;
|
||||
use eframe::egui::Id;
|
||||
use eframe::egui::Layout;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::PointerButton;
|
||||
use eframe::egui::Rgba;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::TextStyle;
|
||||
@@ -46,22 +48,101 @@ use eframe::egui::Visuals;
|
||||
use font_loader::system_fonts;
|
||||
use font_loader::system_fonts::FontPropertyBuilder;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::MonitorNotification;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use komorebi_themes::Base16Value;
|
||||
use komorebi_client::VirtualDesktopNotification;
|
||||
use komorebi_themes::Base16Wrapper;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::CatppuccinValue;
|
||||
use komorebi_themes::KomobarThemeBase16;
|
||||
use komorebi_themes::KomobarThemeCatppuccin;
|
||||
use komorebi_themes::KomobarThemeCustom;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Error;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Write;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ChildStdin;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
lazy_static! {
|
||||
static ref SESSION_STDIN: Mutex<Option<ChildStdin>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
fn start_powershell() -> eyre::Result<()> {
|
||||
// found running session, do nothing
|
||||
if SESSION_STDIN.lock().as_mut().is_some() {
|
||||
tracing::debug!("PowerShell session already started");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::debug!("Starting PowerShell session");
|
||||
|
||||
let mut child = Command::new("powershell.exe")
|
||||
.args(["-NoLogo", "-NoProfile", "-Command", "-"])
|
||||
.stdin(Stdio::piped())
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn()?;
|
||||
|
||||
let stdin = child.stdin.take().expect("stdin piped");
|
||||
|
||||
// Store stdin for later commands
|
||||
let mut session_stdin = SESSION_STDIN.lock();
|
||||
*session_stdin = Option::from(stdin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_powershell() -> eyre::Result<()> {
|
||||
tracing::debug!("Stopping PowerShell session");
|
||||
|
||||
if let Some(mut session_stdin) = SESSION_STDIN.lock().take() {
|
||||
if let Err(e) = session_stdin.write_all(b"exit\n") {
|
||||
tracing::error!(error = %e, "failed to write exit command to PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
if let Err(e) = session_stdin.flush() {
|
||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
tracing::debug!("PowerShell session stopped");
|
||||
} else {
|
||||
tracing::debug!("PowerShell session already stopped");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_powershell(cmd: &str) -> eyre::Result<()> {
|
||||
if let Some(session_stdin) = SESSION_STDIN.lock().as_mut() {
|
||||
if let Err(e) = writeln!(session_stdin, "{cmd}") {
|
||||
tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
if let Err(e) = session_stdin.flush() {
|
||||
tracing::error!(error = %e, "failed to flush PowerShell stdin");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(Error::new(ErrorKind::NotFound, "PowerShell session not started").into())
|
||||
}
|
||||
|
||||
pub struct Komobar {
|
||||
pub hwnd: Option<isize>,
|
||||
@@ -69,7 +150,7 @@ pub struct Komobar {
|
||||
pub disabled: bool,
|
||||
pub config: KomobarConfig,
|
||||
pub render_config: Rc<RefCell<RenderConfig>>,
|
||||
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
pub monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub center_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
||||
@@ -81,6 +162,18 @@ pub struct Komobar {
|
||||
pub size_rect: komorebi_client::Rect,
|
||||
pub work_area_offset: komorebi_client::Rect,
|
||||
applied_theme_on_first_frame: bool,
|
||||
mouse_follows_focus: bool,
|
||||
input_config: InputConfig,
|
||||
}
|
||||
|
||||
struct InputConfig {
|
||||
accumulated_scroll_delta: Vec2,
|
||||
act_on_vertical_scroll: bool,
|
||||
act_on_horizontal_scroll: bool,
|
||||
vertical_scroll_threshold: f32,
|
||||
horizontal_scroll_threshold: f32,
|
||||
vertical_scroll_max_threshold: f32,
|
||||
horizontal_scroll_max_threshold: f32,
|
||||
}
|
||||
|
||||
pub fn apply_theme(
|
||||
@@ -93,12 +186,12 @@ pub fn apply_theme(
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
let (auto_select_fill, auto_select_text) = match theme {
|
||||
KomobarTheme::Catppuccin {
|
||||
KomobarTheme::Catppuccin(KomobarThemeCatppuccin {
|
||||
name: catppuccin,
|
||||
accent: catppuccin_value,
|
||||
auto_select_fill: catppuccin_auto_select_fill,
|
||||
auto_select_text: catppuccin_auto_select_text,
|
||||
} => {
|
||||
}) => {
|
||||
match catppuccin {
|
||||
Catppuccin::Frappe => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
|
||||
@@ -163,12 +256,12 @@ pub fn apply_theme(
|
||||
catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())),
|
||||
)
|
||||
}
|
||||
KomobarTheme::Base16 {
|
||||
KomobarTheme::Base16(KomobarThemeBase16 {
|
||||
name: base16,
|
||||
accent: base16_value,
|
||||
auto_select_fill: base16_auto_select_fill,
|
||||
auto_select_text: base16_auto_select_text,
|
||||
} => {
|
||||
}) => {
|
||||
ctx.set_style(base16.style());
|
||||
let base16_value = base16_value.unwrap_or_default();
|
||||
let accent = base16_value.color32(Base16Wrapper::Base16(base16));
|
||||
@@ -186,12 +279,12 @@ pub fn apply_theme(
|
||||
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))),
|
||||
)
|
||||
}
|
||||
KomobarTheme::Custom {
|
||||
KomobarTheme::Custom(KomobarThemeCustom {
|
||||
colours,
|
||||
accent: base16_value,
|
||||
auto_select_fill: base16_auto_select_fill,
|
||||
auto_select_text: base16_auto_select_text,
|
||||
} => {
|
||||
}) => {
|
||||
let background = colours.background();
|
||||
ctx.set_style(colours.style());
|
||||
let base16_value = base16_value.unwrap_or_default();
|
||||
@@ -229,16 +322,15 @@ pub fn apply_theme(
|
||||
// apply rounding to the widgets
|
||||
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) =
|
||||
&grouping
|
||||
&& let Some(rounding) = config.rounding
|
||||
{
|
||||
if let Some(rounding) = config.rounding {
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
}
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
}
|
||||
|
||||
// Update RenderConfig's background_color so that widgets will have the new color
|
||||
@@ -249,7 +341,7 @@ impl Komobar {
|
||||
pub fn apply_config(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
previous_monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
||||
) {
|
||||
MAX_LABEL_WIDTH.store(
|
||||
self.config.max_label_width.unwrap_or(400.0) as i32,
|
||||
@@ -278,7 +370,7 @@ impl Komobar {
|
||||
self.config.icon_scale,
|
||||
));
|
||||
|
||||
let mut komorebi_notification_state = previous_notification_state;
|
||||
let mut monitor_info = previous_monitor_info;
|
||||
let mut komorebi_widgets = Vec::new();
|
||||
|
||||
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
|
||||
@@ -330,19 +422,18 @@ impl Komobar {
|
||||
komorebi_widgets
|
||||
.into_iter()
|
||||
.for_each(|(mut widget, idx, side)| {
|
||||
match komorebi_notification_state {
|
||||
match monitor_info {
|
||||
None => {
|
||||
komorebi_notification_state =
|
||||
Some(widget.komorebi_notification_state.clone());
|
||||
monitor_info = Some(widget.monitor_info.clone());
|
||||
}
|
||||
Some(ref previous) => {
|
||||
if widget.workspaces.is_some_and(|w| w.enable) {
|
||||
previous.borrow_mut().update_from_config(
|
||||
&widget.komorebi_notification_state.borrow(),
|
||||
);
|
||||
if widget.workspaces.is_some() {
|
||||
previous
|
||||
.borrow_mut()
|
||||
.update_from_self(&widget.monitor_info.borrow());
|
||||
}
|
||||
|
||||
widget.komorebi_notification_state = previous.clone();
|
||||
widget.monitor_info = previous.clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,20 +453,25 @@ impl Komobar {
|
||||
self.right_widgets = right_widgets;
|
||||
|
||||
let (usr_monitor_index, config_work_area_offset) = match &self.config.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
|
||||
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
|
||||
(monitor_config.index, monitor_config.work_area_offset)
|
||||
}
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None),
|
||||
None => (0, None),
|
||||
};
|
||||
let monitor_index = self.komorebi_notification_state.as_ref().and_then(|state| {
|
||||
state
|
||||
.borrow()
|
||||
.monitor_usr_idx_map
|
||||
.get(&usr_monitor_index)
|
||||
.copied()
|
||||
|
||||
let mapped_info = self.monitor_info.as_ref().map(|info| {
|
||||
let monitor = info.borrow();
|
||||
(
|
||||
monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
|
||||
monitor.mouse_follows_focus,
|
||||
)
|
||||
});
|
||||
|
||||
self.monitor_index = monitor_index;
|
||||
if let Some(info) = mapped_info {
|
||||
self.monitor_index = info.0;
|
||||
self.mouse_follows_focus = info.1;
|
||||
}
|
||||
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
|
||||
@@ -426,17 +522,51 @@ impl Komobar {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.komorebi_notification_state.is_some() && !self.disabled {
|
||||
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
|
||||
} else if self.monitor_info.is_some() && !self.disabled {
|
||||
tracing::warn!(
|
||||
"couldn't find the monitor index of this bar! Disabling the bar until the monitor connects..."
|
||||
);
|
||||
self.disabled = true;
|
||||
} else {
|
||||
tracing::warn!("couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi.");
|
||||
tracing::warn!(
|
||||
"couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi."
|
||||
);
|
||||
self.disabled = true;
|
||||
}
|
||||
|
||||
if let Some(mouse) = &self.config.mouse {
|
||||
self.input_config.act_on_vertical_scroll =
|
||||
mouse.on_scroll_up.is_some() || mouse.on_scroll_down.is_some();
|
||||
self.input_config.act_on_horizontal_scroll =
|
||||
mouse.on_scroll_left.is_some() || mouse.on_scroll_right.is_some();
|
||||
self.input_config.vertical_scroll_threshold = mouse
|
||||
.vertical_scroll_threshold
|
||||
.unwrap_or(30.0)
|
||||
.clamp(10.0, 300.0);
|
||||
self.input_config.horizontal_scroll_threshold = mouse
|
||||
.horizontal_scroll_threshold
|
||||
.unwrap_or(30.0)
|
||||
.clamp(10.0, 300.0);
|
||||
// limit how many "ticks" can be accumulated
|
||||
self.input_config.vertical_scroll_max_threshold =
|
||||
self.input_config.vertical_scroll_threshold * 3.0;
|
||||
self.input_config.horizontal_scroll_max_threshold =
|
||||
self.input_config.horizontal_scroll_threshold * 3.0;
|
||||
|
||||
if mouse.has_command() {
|
||||
start_powershell().unwrap_or_else(|_| {
|
||||
tracing::error!("failed to start powershell session");
|
||||
});
|
||||
} else {
|
||||
stop_powershell().unwrap_or_else(|_| {
|
||||
tracing::error!("failed to stop powershell session");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("widget configuration options applied");
|
||||
|
||||
self.komorebi_notification_state = komorebi_notification_state;
|
||||
self.monitor_info = monitor_info;
|
||||
}
|
||||
|
||||
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
|
||||
@@ -473,7 +603,9 @@ impl Komobar {
|
||||
end.x -= margin.left + margin.right;
|
||||
|
||||
if end.y == 0.0 {
|
||||
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
|
||||
tracing::warn!(
|
||||
"position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default"
|
||||
)
|
||||
}
|
||||
|
||||
self.size_rect = komorebi_client::Rect {
|
||||
@@ -505,8 +637,7 @@ impl Komobar {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
home
|
||||
@@ -520,26 +651,6 @@ impl Komobar {
|
||||
match komorebi_client::StaticConfig::read(&config) {
|
||||
Ok(config) => {
|
||||
if let Some(theme) = config.theme {
|
||||
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(Base16Wrapper::Base16(name)),
|
||||
KomorebiTheme::Custom {
|
||||
ref colours,
|
||||
stack_border,
|
||||
..
|
||||
} => stack_border
|
||||
.unwrap_or(Base16Value::Base0B)
|
||||
.color32(Base16Wrapper::Custom(colours.clone())),
|
||||
};
|
||||
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
@@ -549,10 +660,6 @@ impl Komobar {
|
||||
bar_grouping,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
|
||||
if let Some(state) = &self.komorebi_notification_state {
|
||||
state.borrow_mut().stack_accent = Some(stack_accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -565,17 +672,16 @@ impl Komobar {
|
||||
| Grouping::Alignment(config)
|
||||
| Grouping::Widget(config),
|
||||
) = &bar_grouping
|
||||
&& let Some(rounding) = config.rounding
|
||||
{
|
||||
if let Some(rounding) = config.rounding {
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius =
|
||||
rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
}
|
||||
ctx.style_mut(|style| {
|
||||
style.visuals.widgets.noninteractive.corner_radius =
|
||||
rounding.into();
|
||||
style.visuals.widgets.inactive.corner_radius = rounding.into();
|
||||
style.visuals.widgets.hovered.corner_radius = rounding.into();
|
||||
style.visuals.widgets.active.corner_radius = rounding.into();
|
||||
style.visuals.widgets.open.corner_radius = rounding.into();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,7 +701,7 @@ impl Komobar {
|
||||
disabled: false,
|
||||
config,
|
||||
render_config: Rc::new(RefCell::new(RenderConfig::new())),
|
||||
komorebi_notification_state: None,
|
||||
monitor_info: None,
|
||||
left_widgets: vec![],
|
||||
center_widgets: vec![],
|
||||
right_widgets: vec![],
|
||||
@@ -607,6 +713,16 @@ impl Komobar {
|
||||
size_rect: komorebi_client::Rect::default(),
|
||||
work_area_offset: komorebi_client::Rect::default(),
|
||||
applied_theme_on_first_frame: false,
|
||||
mouse_follows_focus: false,
|
||||
input_config: InputConfig {
|
||||
accumulated_scroll_delta: Vec2::new(0.0, 0.0),
|
||||
act_on_vertical_scroll: false,
|
||||
act_on_horizontal_scroll: false,
|
||||
vertical_scroll_threshold: 0.0,
|
||||
horizontal_scroll_threshold: 0.0,
|
||||
vertical_scroll_max_threshold: 0.0,
|
||||
horizontal_scroll_max_threshold: 0.0,
|
||||
},
|
||||
};
|
||||
|
||||
komobar.apply_config(&cc.egui_ctx, None);
|
||||
@@ -731,12 +847,12 @@ impl eframe::App for Komobar {
|
||||
|
||||
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.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
}
|
||||
|
||||
if let Ok(updated_config) = self.rx_config.try_recv() {
|
||||
self.config = updated_config;
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
}
|
||||
|
||||
match self.rx_gui.try_recv() {
|
||||
@@ -751,13 +867,41 @@ impl eframe::App for Komobar {
|
||||
Ok(KomorebiEvent::Notification(notification)) => {
|
||||
let state = ¬ification.state;
|
||||
let usr_monitor_index = match &self.config.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
|
||||
MonitorConfigOrIndex::Index(idx) => *idx,
|
||||
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
|
||||
monitor_config.index
|
||||
}
|
||||
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied();
|
||||
self.monitor_index = monitor_index;
|
||||
let mut should_apply_config = false;
|
||||
|
||||
match notification.event {
|
||||
NotificationEvent::VirtualDesktop(
|
||||
VirtualDesktopNotification::EnteredAssociatedVirtualDesktop,
|
||||
) => {
|
||||
tracing::debug!(
|
||||
"back on komorebi's associated virtual desktop - restoring bar"
|
||||
);
|
||||
if let Some(hwnd) = self.hwnd {
|
||||
komorebi_client::WindowsApi::restore_window(hwnd);
|
||||
}
|
||||
}
|
||||
NotificationEvent::VirtualDesktop(
|
||||
VirtualDesktopNotification::LeftAssociatedVirtualDesktop,
|
||||
) => {
|
||||
tracing::debug!(
|
||||
"no longer on komorebi's associated virtual desktop - minimizing bar"
|
||||
);
|
||||
if let Some(hwnd) = self.hwnd {
|
||||
komorebi_client::WindowsApi::minimize_window(hwnd);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.monitor_index.is_none()
|
||||
|| self
|
||||
.monitor_index
|
||||
@@ -802,9 +946,9 @@ impl eframe::App for Komobar {
|
||||
) {
|
||||
let monitor_index = self.monitor_index.expect("should have a monitor index");
|
||||
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size();
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size;
|
||||
|
||||
self.update_monitor_coordinates(monitor_size);
|
||||
self.update_monitor_coordinates(&monitor_size);
|
||||
|
||||
should_apply_config = true;
|
||||
}
|
||||
@@ -815,7 +959,7 @@ impl eframe::App for Komobar {
|
||||
|
||||
// Check if monitor coordinates/size has changed
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size();
|
||||
let monitor_size = state.monitors.elements()[monitor_index].size;
|
||||
let top = MONITOR_TOP.load(Ordering::SeqCst);
|
||||
let left = MONITOR_LEFT.load(Ordering::SeqCst);
|
||||
let right = MONITOR_RIGHT.load(Ordering::SeqCst);
|
||||
@@ -825,36 +969,38 @@ impl eframe::App for Komobar {
|
||||
bottom: monitor_size.bottom,
|
||||
right,
|
||||
};
|
||||
if *monitor_size != rect {
|
||||
if monitor_size != rect {
|
||||
tracing::info!(
|
||||
"Monitor coordinates/size has changed, storing new coordinates: {:#?}",
|
||||
monitor_size
|
||||
);
|
||||
|
||||
self.update_monitor_coordinates(monitor_size);
|
||||
self.update_monitor_coordinates(&monitor_size);
|
||||
|
||||
should_apply_config = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
|
||||
komorebi_notification_state
|
||||
.borrow_mut()
|
||||
.handle_notification(
|
||||
ctx,
|
||||
self.monitor_index,
|
||||
notification,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme.clone(),
|
||||
self.render_config.clone(),
|
||||
);
|
||||
if let Some(monitor_info) = &self.monitor_info {
|
||||
monitor_info.borrow_mut().update(
|
||||
self.monitor_index,
|
||||
notification.state,
|
||||
self.render_config.borrow().show_all_icons,
|
||||
);
|
||||
handle_notification(
|
||||
ctx,
|
||||
notification.event,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme.clone(),
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if should_apply_config {
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
|
||||
// Reposition the Bar
|
||||
self.position_bar();
|
||||
@@ -936,6 +1082,111 @@ impl eframe::App for Komobar {
|
||||
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
|
||||
|
||||
CentralPanel::default().frame(frame).show(ctx, |ui| {
|
||||
if let Some(mouse_config) = &self.config.mouse {
|
||||
let command = if ui
|
||||
.input(|i| i.pointer.button_double_clicked(PointerButton::Primary))
|
||||
{
|
||||
tracing::debug!("Input: primary button double clicked");
|
||||
&mouse_config.on_primary_double_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Secondary)) {
|
||||
tracing::debug!("Input: secondary button clicked");
|
||||
&mouse_config.on_secondary_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Middle)) {
|
||||
tracing::debug!("Input: middle button clicked");
|
||||
&mouse_config.on_middle_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Extra1)) {
|
||||
tracing::debug!("Input: extra1 button clicked");
|
||||
&mouse_config.on_extra1_click
|
||||
} else if ui.input(|i| i.pointer.button_clicked(PointerButton::Extra2)) {
|
||||
tracing::debug!("Input: extra2 button clicked");
|
||||
&mouse_config.on_extra2_click
|
||||
} else if self.input_config.act_on_vertical_scroll
|
||||
|| self.input_config.act_on_horizontal_scroll
|
||||
{
|
||||
let scroll_delta = ui.input(|input| input.smooth_scroll_delta);
|
||||
|
||||
self.input_config.accumulated_scroll_delta += scroll_delta;
|
||||
|
||||
if scroll_delta.y != 0.0 && self.input_config.act_on_vertical_scroll {
|
||||
// Do not store more than the max threshold
|
||||
self.input_config.accumulated_scroll_delta.y =
|
||||
self.input_config.accumulated_scroll_delta.y.clamp(
|
||||
-self.input_config.vertical_scroll_max_threshold,
|
||||
self.input_config.vertical_scroll_max_threshold,
|
||||
);
|
||||
|
||||
// When the accumulated scroll passes the threshold, trigger a tick.
|
||||
if self.input_config.accumulated_scroll_delta.y.abs()
|
||||
>= self.input_config.vertical_scroll_threshold
|
||||
{
|
||||
let direction_command =
|
||||
if self.input_config.accumulated_scroll_delta.y > 0.0 {
|
||||
&mouse_config.on_scroll_up
|
||||
} else {
|
||||
&mouse_config.on_scroll_down
|
||||
};
|
||||
|
||||
// Remove one tick's worth of scroll from the accumulator, preserving any excess.
|
||||
self.input_config.accumulated_scroll_delta.y -=
|
||||
self.input_config.vertical_scroll_threshold
|
||||
* self.input_config.accumulated_scroll_delta.y.signum();
|
||||
|
||||
tracing::debug!(
|
||||
"Input: vertical scroll ticked. excess: {} | threshold: {}",
|
||||
self.input_config.accumulated_scroll_delta.y,
|
||||
self.input_config.vertical_scroll_threshold
|
||||
);
|
||||
|
||||
direction_command
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
} else if scroll_delta.x != 0.0 && self.input_config.act_on_horizontal_scroll {
|
||||
// Do not store more than the max threshold
|
||||
self.input_config.accumulated_scroll_delta.x =
|
||||
self.input_config.accumulated_scroll_delta.x.clamp(
|
||||
-self.input_config.horizontal_scroll_max_threshold,
|
||||
self.input_config.horizontal_scroll_max_threshold,
|
||||
);
|
||||
|
||||
// When the accumulated scroll passes the threshold, trigger a tick.
|
||||
if self.input_config.accumulated_scroll_delta.x.abs()
|
||||
>= self.input_config.horizontal_scroll_threshold
|
||||
{
|
||||
let direction_command =
|
||||
if self.input_config.accumulated_scroll_delta.x > 0.0 {
|
||||
&mouse_config.on_scroll_left
|
||||
} else {
|
||||
&mouse_config.on_scroll_right
|
||||
};
|
||||
|
||||
// Remove one tick's worth of scroll from the accumulator, preserving any excess.
|
||||
self.input_config.accumulated_scroll_delta.x -=
|
||||
self.input_config.horizontal_scroll_threshold
|
||||
* self.input_config.accumulated_scroll_delta.x.signum();
|
||||
|
||||
tracing::debug!(
|
||||
"Input: horizontal scroll ticked. excess: {} | threshold: {}",
|
||||
self.input_config.accumulated_scroll_delta.x,
|
||||
self.input_config.horizontal_scroll_threshold
|
||||
);
|
||||
|
||||
direction_command
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
|
||||
if let Some(command) = command {
|
||||
command.execute(self.mouse_follows_focus);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply grouping logic for the bar as a whole
|
||||
let area_frame = if let Some(frame) = &self.config.frame {
|
||||
Frame::NONE
|
||||
@@ -1075,3 +1326,66 @@ pub enum Alignment {
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn handle_notification(
|
||||
ctx: &Context,
|
||||
event: komorebi_client::NotificationEvent,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
bg_color_with_alpha: Rc<RefCell<Color32>>,
|
||||
transparency_alpha: Option<u8>,
|
||||
grouping: Option<Grouping>,
|
||||
default_theme: Option<KomobarTheme>,
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
if let NotificationEvent::Socket(message) = event {
|
||||
match message {
|
||||
SocketMessage::ReloadStaticConfiguration(path) => {
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
} else if let Some(default_theme) = default_theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
default_theme,
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!(
|
||||
"removed theme from updated komorebi.json and applied default theme"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"theme was removed from updated komorebi.json but there was no default theme to apply"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(*theme),
|
||||
bg_color,
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use crate::DEFAULT_PADDING;
|
||||
use crate::bar::exec_powershell;
|
||||
use crate::render::Grouping;
|
||||
use crate::widgets::widget::WidgetConfig;
|
||||
use crate::DEFAULT_PADDING;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -13,9 +15,10 @@ use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.36`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.40`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar height (default: 50)
|
||||
/// Bar height
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50.0)))]
|
||||
pub height: Option<f32>,
|
||||
/// Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or
|
||||
/// vertical definition which can each take a single value for a symmetric padding or two
|
||||
@@ -73,23 +76,31 @@ pub struct KomobarConfig {
|
||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
|
||||
pub frame: Option<FrameConfig>,
|
||||
/// The monitor index or the full monitor options
|
||||
pub monitor: MonitorConfigOrIndex,
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = MonitorConfigOrIndex::Index(0))))]
|
||||
pub monitor: Option<MonitorConfigOrIndex>,
|
||||
/// Font family
|
||||
pub font_family: Option<String>,
|
||||
/// Font size (default: 12.5)
|
||||
/// Font size
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12.5)))]
|
||||
pub font_size: Option<f32>,
|
||||
/// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
|
||||
/// Scale of the icons relative to the font_size [[1.0-2.0]]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 1.4)))]
|
||||
pub icon_scale: Option<f32>,
|
||||
/// Max label width before text truncation (default: 400.0)
|
||||
/// Max label width before text truncation
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 400.0)))]
|
||||
pub max_label_width: Option<f32>,
|
||||
/// Theme
|
||||
pub theme: Option<KomobarTheme>,
|
||||
/// Alpha value for the color transparency [[0-255]] (default: 200)
|
||||
/// Alpha value for the color transparency [[0-255]]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 200)))]
|
||||
pub transparency_alpha: Option<u8>,
|
||||
/// Spacing between widgets (default: 10.0)
|
||||
/// Spacing between widgets
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10.0)))]
|
||||
pub widget_spacing: Option<f32>,
|
||||
/// Visual grouping for widgets
|
||||
pub grouping: Option<Grouping>,
|
||||
/// Options for mouse interaction on the bar
|
||||
pub mouse: Option<MouseConfig>,
|
||||
/// Left side widgets (ordered left-to-right)
|
||||
pub left_widgets: Vec<WidgetConfig>,
|
||||
/// Center widgets (ordered left-to-right)
|
||||
@@ -115,7 +126,9 @@ impl KomobarConfig {
|
||||
}
|
||||
|
||||
if display {
|
||||
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
|
||||
println!(
|
||||
"\nYour bar configuration file contains some options that have been renamed or deprecated:\n"
|
||||
);
|
||||
for (canonical, aliases) in map {
|
||||
for alias in aliases {
|
||||
if raw.contains(alias) {
|
||||
@@ -138,6 +151,7 @@ impl KomobarConfig {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Position configuration
|
||||
pub struct PositionConfig {
|
||||
/// The desired starting position of the bar (0,0 = top left of the screen)
|
||||
#[serde(alias = "position")]
|
||||
@@ -149,6 +163,7 @@ pub struct PositionConfig {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Frame configuration
|
||||
pub struct FrameConfig {
|
||||
/// Margin inside the painted frame
|
||||
pub inner_margin: Position,
|
||||
@@ -157,6 +172,7 @@ pub struct FrameConfig {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Monitor configuration or monitor index
|
||||
pub enum MonitorConfigOrIndex {
|
||||
/// The monitor index where you want the bar to show
|
||||
Index(usize),
|
||||
@@ -166,6 +182,7 @@ pub enum MonitorConfigOrIndex {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Monitor configuration
|
||||
pub struct MonitorConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub index: usize,
|
||||
@@ -183,9 +200,13 @@ pub type Margin = SpacingKind;
|
||||
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
|
||||
// `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the
|
||||
// individual values.
|
||||
/// Spacing kind
|
||||
pub enum SpacingKind {
|
||||
/// Spacing applied to all sides
|
||||
All(f32),
|
||||
/// Individual spacing applied to each side
|
||||
Individual(IndividualSpacingConfig),
|
||||
/// Grouped vertical and horizontal spacing
|
||||
Grouped(GroupedSpacingConfig),
|
||||
}
|
||||
|
||||
@@ -230,25 +251,36 @@ impl SpacingKind {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Grouped vertical and horizontal spacing
|
||||
pub struct GroupedSpacingConfig {
|
||||
/// Vertical grouped spacing
|
||||
pub vertical: Option<GroupedSpacingOptions>,
|
||||
/// Horizontal grouped spacing
|
||||
pub horizontal: Option<GroupedSpacingOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Grouped spacing options
|
||||
pub enum GroupedSpacingOptions {
|
||||
/// Symmetrical grouped spacing
|
||||
Symmetrical(f32),
|
||||
/// Split grouped spacing
|
||||
Split(f32, f32),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Individual spacing configuration
|
||||
pub struct IndividualSpacingConfig {
|
||||
/// Spacing for the top
|
||||
pub top: f32,
|
||||
/// Spacing for the bottom
|
||||
pub bottom: f32,
|
||||
/// Spacing for the left
|
||||
pub left: f32,
|
||||
/// Spacing for the right
|
||||
pub right: f32,
|
||||
}
|
||||
|
||||
@@ -325,6 +357,153 @@ pub fn get_individual_spacing(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Mouse message
|
||||
pub enum MouseMessage {
|
||||
/// Send a message to the komorebi client.
|
||||
/// By default, a batch of messages are sent in the following order:
|
||||
/// FocusMonitorAtCursor =>
|
||||
/// MouseFollowsFocus(false) =>
|
||||
/// {message} =>
|
||||
/// MouseFollowsFocus({original.value})
|
||||
///
|
||||
/// Example:
|
||||
/// ```json
|
||||
/// "on_extra2_click": {
|
||||
/// "message": {
|
||||
/// "type": "NewWorkspace"
|
||||
/// }
|
||||
/// },
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "on_middle_click": {
|
||||
/// "focus_monitor_at_cursor": false,
|
||||
/// "ignore_mouse_follows_focus": false,
|
||||
/// "message": {
|
||||
/// "type": "TogglePause"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// or:
|
||||
/// ```json
|
||||
/// "on_scroll_up": {
|
||||
/// "message": {
|
||||
/// "type": "CycleFocusWorkspace",
|
||||
/// "content": "Previous"
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
Komorebi(KomorebiMouseMessage),
|
||||
/// Execute a custom command.
|
||||
/// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved.
|
||||
/// Example: `komorebic toggle-pause`
|
||||
Command(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Komorebi socket mouse message
|
||||
pub struct KomorebiMouseMessage {
|
||||
/// Send the FocusMonitorAtCursor message
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||
pub focus_monitor_at_cursor: Option<bool>,
|
||||
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||
pub ignore_mouse_follows_focus: Option<bool>,
|
||||
/// The message to send to the komorebi client
|
||||
pub message: komorebi_client::SocketMessage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Mouse configuration
|
||||
pub struct MouseConfig {
|
||||
/// Command to send on primary/left double button click
|
||||
pub on_primary_double_click: Option<MouseMessage>,
|
||||
/// Command to send on secondary/right button click
|
||||
pub on_secondary_click: Option<MouseMessage>,
|
||||
/// Command to send on middle button click
|
||||
pub on_middle_click: Option<MouseMessage>,
|
||||
/// Command to send on extra1/back button click
|
||||
pub on_extra1_click: Option<MouseMessage>,
|
||||
/// Command to send on extra2/forward button click
|
||||
pub on_extra2_click: Option<MouseMessage>,
|
||||
|
||||
/// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 30.0)))]
|
||||
pub vertical_scroll_threshold: Option<f32>,
|
||||
/// Command to send on scrolling up (every tick)
|
||||
pub on_scroll_up: Option<MouseMessage>,
|
||||
/// Command to send on scrolling down (every tick)
|
||||
pub on_scroll_down: Option<MouseMessage>,
|
||||
|
||||
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 30.0)))]
|
||||
pub horizontal_scroll_threshold: Option<f32>,
|
||||
/// Command to send on scrolling left (every tick)
|
||||
pub on_scroll_left: Option<MouseMessage>,
|
||||
/// Command to send on scrolling right (every tick)
|
||||
pub on_scroll_right: Option<MouseMessage>,
|
||||
}
|
||||
|
||||
impl MouseConfig {
|
||||
pub fn has_command(&self) -> bool {
|
||||
[
|
||||
&self.on_primary_double_click,
|
||||
&self.on_secondary_click,
|
||||
&self.on_middle_click,
|
||||
&self.on_extra1_click,
|
||||
&self.on_extra2_click,
|
||||
&self.on_scroll_up,
|
||||
&self.on_scroll_down,
|
||||
&self.on_scroll_left,
|
||||
&self.on_scroll_right,
|
||||
]
|
||||
.iter()
|
||||
.any(|opt| matches!(opt, Some(MouseMessage::Command(_))))
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseMessage {
|
||||
pub fn execute(&self, mouse_follows_focus: bool) {
|
||||
match self {
|
||||
MouseMessage::Komorebi(config) => {
|
||||
let mut messages = Vec::new();
|
||||
|
||||
if config.focus_monitor_at_cursor.unwrap_or(true) {
|
||||
messages.push(SocketMessage::FocusMonitorAtCursor);
|
||||
}
|
||||
|
||||
if config.ignore_mouse_follows_focus.unwrap_or(true) {
|
||||
messages.push(SocketMessage::MouseFollowsFocus(false));
|
||||
messages.push(config.message.clone());
|
||||
messages.push(SocketMessage::MouseFollowsFocus(mouse_follows_focus));
|
||||
} else {
|
||||
messages.push(config.message.clone());
|
||||
}
|
||||
|
||||
tracing::debug!("Sending messages: {messages:?}");
|
||||
|
||||
if komorebi_client::send_batch(messages).is_err() {
|
||||
tracing::error!("could not send commands");
|
||||
}
|
||||
}
|
||||
MouseMessage::Command(cmd) => {
|
||||
tracing::debug!("Executing command: {}", cmd);
|
||||
|
||||
let cmd_no_env = cmd.replace_env();
|
||||
|
||||
if exec_powershell(cmd_no_env.to_str().expect("Invalid command")).is_err() {
|
||||
tracing::error!("Failed to execute '{}'", cmd);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl KomobarConfig {
|
||||
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
@@ -348,6 +527,7 @@ impl KomobarConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Position
|
||||
pub struct Position {
|
||||
/// X coordinate
|
||||
pub x: f32,
|
||||
@@ -373,71 +553,11 @@ impl From<Position> for Pos2 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
pub enum KomobarTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
auto_select_fill: Option<komorebi_themes::CatppuccinValue>,
|
||||
auto_select_text: Option<komorebi_themes::CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
name: komorebi_themes::Base16,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_fill: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_text: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
/// A custom Base16 theme
|
||||
Custom {
|
||||
/// Colours of the custom Base16 theme palette
|
||||
colours: Box<komorebi_themes::Base16ColourPalette>,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_fill: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_text: 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,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
KomorebiTheme::Base16 {
|
||||
name, bar_accent, ..
|
||||
} => Self::Base16 {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
KomorebiTheme::Custom {
|
||||
colours,
|
||||
bar_accent,
|
||||
..
|
||||
} => Self::Custom {
|
||||
colours,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use komorebi_themes::KomobarTheme;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Label prefix
|
||||
pub enum LabelPrefix {
|
||||
/// Show no prefix
|
||||
None,
|
||||
@@ -451,6 +571,7 @@ pub enum LabelPrefix {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Display format
|
||||
pub enum DisplayFormat {
|
||||
/// Show only icon
|
||||
Icon,
|
||||
@@ -465,9 +586,10 @@ pub enum DisplayFormat {
|
||||
}
|
||||
|
||||
macro_rules! extend_enum {
|
||||
($existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
|
||||
($(#[$type_meta:meta])* $existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
|
||||
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
$(#[$type_meta])*
|
||||
pub enum $new_enum {
|
||||
// Add new variants
|
||||
$(
|
||||
@@ -488,7 +610,9 @@ macro_rules! extend_enum {
|
||||
};
|
||||
}
|
||||
|
||||
extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
|
||||
extend_enum!(
|
||||
/// Workspaces display format
|
||||
DisplayFormat, WorkspacesDisplayFormat, {
|
||||
/// Show all icons only
|
||||
AllIcons,
|
||||
/// Show both all icons and text
|
||||
|
||||
@@ -15,12 +15,10 @@ use eframe::egui::ViewportBuilder;
|
||||
use font_loader::system_fonts;
|
||||
use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use image::RgbaImage;
|
||||
use komorebi_client::replace_env_in_path;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::SubscribeOptions;
|
||||
use std::collections::HashMap;
|
||||
use komorebi_client::replace_env_in_path;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
@@ -28,16 +26,14 @@ use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
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::HiDpi::SetProcessDpiAwarenessContext;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
use windows_core::BOOL;
|
||||
@@ -53,9 +49,6 @@ pub static DEFAULT_PADDING: f32 = 10.0;
|
||||
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version)]
|
||||
struct Opts {
|
||||
@@ -110,7 +103,7 @@ fn process_hwnd() -> Option<isize> {
|
||||
}
|
||||
|
||||
pub enum KomorebiEvent {
|
||||
Notification(komorebi_client::Notification),
|
||||
Notification(Box<komorebi_client::Notification>),
|
||||
Reconnect,
|
||||
}
|
||||
|
||||
@@ -121,15 +114,8 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
if opts.schema {
|
||||
let settings = schemars::gen::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)?;
|
||||
let bar_config = schemars::schema_for!(KomobarConfig);
|
||||
let schema = serde_json::to_string_pretty(&bar_config)?;
|
||||
|
||||
println!("{schema}");
|
||||
std::process::exit(0);
|
||||
@@ -144,13 +130,17 @@ fn main() -> color_eyre::Result<()> {
|
||||
}
|
||||
|
||||
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
unsafe {
|
||||
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");
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
}
|
||||
|
||||
tracing::subscriber::set_global_default(
|
||||
@@ -166,8 +156,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
home
|
||||
@@ -227,28 +216,30 @@ fn main() -> color_eyre::Result<()> {
|
||||
)?)?;
|
||||
|
||||
let (usr_monitor_index, work_area_offset) = match &config.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
|
||||
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
|
||||
(monitor_config.index, monitor_config.work_area_offset)
|
||||
}
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None),
|
||||
None => (0, None),
|
||||
};
|
||||
|
||||
let monitor_index = state
|
||||
.monitor_usr_idx_map
|
||||
.get(&usr_monitor_index)
|
||||
.map_or(usr_monitor_index, |i| *i);
|
||||
|
||||
MONITOR_RIGHT.store(
|
||||
state.monitors.elements()[monitor_index].size().right,
|
||||
state.monitors.elements()[monitor_index].size.right,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_TOP.store(
|
||||
state.monitors.elements()[monitor_index].size().top,
|
||||
state.monitors.elements()[monitor_index].size.top,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
MONITOR_LEFT.store(
|
||||
state.monitors.elements()[monitor_index].size().left,
|
||||
state.monitors.elements()[monitor_index].size.left,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
@@ -258,11 +249,11 @@ fn main() -> color_eyre::Result<()> {
|
||||
None => {
|
||||
config.position = Some(PositionConfig {
|
||||
start: Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
||||
x: state.monitors.elements()[monitor_index].size.left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size.top as f32,
|
||||
}),
|
||||
end: Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size().right as f32,
|
||||
x: state.monitors.elements()[monitor_index].size.right as f32,
|
||||
y: 50.0,
|
||||
}),
|
||||
})
|
||||
@@ -270,14 +261,14 @@ fn main() -> color_eyre::Result<()> {
|
||||
Some(ref mut position) => {
|
||||
if position.start.is_none() {
|
||||
position.start = Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size().left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size().top as f32,
|
||||
x: state.monitors.elements()[monitor_index].size.left as f32,
|
||||
y: state.monitors.elements()[monitor_index].size.top as f32,
|
||||
});
|
||||
}
|
||||
|
||||
if position.end.is_none() {
|
||||
position.end = Some(Position {
|
||||
x: state.monitors.elements()[monitor_index].size().right as f32,
|
||||
x: state.monitors.elements()[monitor_index].size.right as f32,
|
||||
y: 50.0,
|
||||
})
|
||||
}
|
||||
@@ -362,7 +353,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
while komorebi_client::send_message(
|
||||
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
|
||||
)
|
||||
.is_err()
|
||||
.is_err()
|
||||
{
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
@@ -385,7 +376,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
Ok(notification) => {
|
||||
tracing::debug!("received notification from komorebi");
|
||||
|
||||
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
|
||||
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(Box::new(notification))) {
|
||||
tracing::error!("could not send komorebi notification update to gui thread: {error}")
|
||||
}
|
||||
|
||||
@@ -413,5 +404,5 @@ fn main() -> color_eyre::Result<()> {
|
||||
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
|
||||
}),
|
||||
)
|
||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::KomobarConfig;
|
||||
use crate::config::MonitorConfigOrIndex;
|
||||
use crate::AUTO_SELECT_FILL_COLOUR;
|
||||
use crate::AUTO_SELECT_TEXT_COLOUR;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
@@ -18,27 +18,33 @@ use komorebi_client::Rgb;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::num::NonZeroU32;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "kind")]
|
||||
/// Grouping
|
||||
pub enum Grouping {
|
||||
/// No grouping is applied
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "None"))]
|
||||
None,
|
||||
/// Widgets are grouped as a whole
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Bar"))]
|
||||
Bar(GroupingConfig),
|
||||
/// Widgets are grouped by alignment
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Alignment"))]
|
||||
Alignment(GroupingConfig),
|
||||
/// Widgets are grouped individually
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Widget"))]
|
||||
Widget(GroupingConfig),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Render configuration
|
||||
pub struct RenderConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub monitor_idx: usize,
|
||||
@@ -93,8 +99,9 @@ impl RenderExt for &KomobarConfig {
|
||||
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
|
||||
|
||||
let monitor_idx = match &self.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
|
||||
MonitorConfigOrIndex::Index(idx) => *idx,
|
||||
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => monitor_config.index,
|
||||
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
|
||||
@@ -356,6 +363,7 @@ impl RenderConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Grouping configuration
|
||||
pub struct GroupingConfig {
|
||||
/// Styles for the grouping
|
||||
pub style: Option<GroupingStyle>,
|
||||
@@ -367,7 +375,9 @@ pub struct GroupingConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Grouping Style
|
||||
pub enum GroupingStyle {
|
||||
/// Default
|
||||
#[serde(alias = "CtByte")]
|
||||
Default,
|
||||
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
|
||||
@@ -389,8 +399,9 @@ pub enum GroupingStyle {
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Rounding configuration
|
||||
pub enum RoundingConfig {
|
||||
/// All 4 corners are the same
|
||||
/// All 4 corners are the same
|
||||
Same(f32),
|
||||
/// All 4 corners are custom. Order: NW, NE, SW, SE
|
||||
Individual([f32; 4]),
|
||||
|
||||
409
komorebi-bar/src/widgets/applications.rs
Normal file
409
komorebi-bar/src/widgets/applications.rs
Normal file
@@ -0,0 +1,409 @@
|
||||
use super::ImageIcon;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
use eframe::egui::Frame;
|
||||
use eframe::egui::Image;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::Margin;
|
||||
use eframe::egui::RichText;
|
||||
use eframe::egui::Sense;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::vec2;
|
||||
use komorebi_client::PathExt;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tracing;
|
||||
use which::which;
|
||||
|
||||
/// Minimum interval between consecutive application launches to prevent accidental spamming.
|
||||
const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Applications widget configuration
|
||||
pub struct ApplicationsConfig {
|
||||
/// Enables or disables the applications widget.
|
||||
pub enable: bool,
|
||||
/// Whether to show the launch command on hover (optional).
|
||||
/// Could be overridden per application. Defaults to `false` if not set.
|
||||
pub show_command_on_hover: Option<bool>,
|
||||
/// Horizontal spacing between application buttons.
|
||||
pub spacing: Option<f32>,
|
||||
/// Default display format for all applications (optional).
|
||||
/// Could be overridden per application. Defaults to `Icon`.
|
||||
pub display: Option<ApplicationsDisplayFormat>,
|
||||
/// List of configured applications to display.
|
||||
pub items: Vec<AppConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Application button configuration
|
||||
pub struct AppConfig {
|
||||
/// Whether to enable this application button (optional).
|
||||
/// Inherits from the global `Applications` setting if omitted.
|
||||
pub enable: Option<bool>,
|
||||
/// Whether to show the launch command on hover (optional).
|
||||
/// Inherits from the global `Applications` setting if omitted.
|
||||
pub show_command_on_hover: Option<bool>,
|
||||
/// Display name of the application.
|
||||
pub name: String,
|
||||
/// Optional icon: a path to an image or a text-based glyph (e.g., from Nerd Fonts).
|
||||
/// If not set, and if the `command` is a path to an executable, an icon might be extracted from it.
|
||||
/// Note: glyphs require a compatible `font_family`.
|
||||
pub icon: Option<String>,
|
||||
/// Command to execute (e.g. path to the application or shell command).
|
||||
pub command: String,
|
||||
/// Display format for this application button (optional). Overrides global format if set.
|
||||
pub display: Option<ApplicationsDisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Applications widget display format
|
||||
pub enum ApplicationsDisplayFormat {
|
||||
/// Show only the application icon.
|
||||
#[default]
|
||||
Icon,
|
||||
/// Show only the application name as text.
|
||||
Text,
|
||||
/// Show both the application icon and name.
|
||||
IconAndText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Applications {
|
||||
/// Whether the applications widget is enabled.
|
||||
pub enable: bool,
|
||||
/// Horizontal spacing between application buttons.
|
||||
pub spacing: Option<f32>,
|
||||
/// Applications to be rendered in the UI.
|
||||
pub items: Vec<App>,
|
||||
}
|
||||
|
||||
impl BarWidget for Applications {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if !self.enable {
|
||||
return;
|
||||
}
|
||||
|
||||
let icon_config = IconConfig {
|
||||
font_id: config.icon_font_id.clone(),
|
||||
size: config.icon_font_id.size,
|
||||
color: ctx.style().visuals.selection.stroke.color,
|
||||
};
|
||||
|
||||
if let Some(spacing) = self.spacing {
|
||||
ui.spacing_mut().item_spacing.x = spacing;
|
||||
}
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
for app in &mut self.items {
|
||||
app.render(ctx, ui, &icon_config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ApplicationsConfig> for Applications {
|
||||
fn from(applications_config: &ApplicationsConfig) -> Self {
|
||||
let items = applications_config
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, config)| {
|
||||
let command = UserCommand::new(&config.command);
|
||||
|
||||
App {
|
||||
enable: config.enable.unwrap_or(applications_config.enable),
|
||||
#[allow(clippy::obfuscated_if_else)]
|
||||
name: config
|
||||
.name
|
||||
.is_empty()
|
||||
.then(|| format!("App {}", index + 1))
|
||||
.unwrap_or_else(|| config.name.clone()),
|
||||
icon: Icon::try_from_path(config.icon.as_deref())
|
||||
.or_else(|| Icon::try_from_command(&command)),
|
||||
command,
|
||||
display: config
|
||||
.display
|
||||
.or(applications_config.display)
|
||||
.unwrap_or_default(),
|
||||
show_command_on_hover: config
|
||||
.show_command_on_hover
|
||||
.or(applications_config.show_command_on_hover)
|
||||
.unwrap_or(false),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
enable: applications_config.enable,
|
||||
items,
|
||||
spacing: applications_config.spacing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single resolved application entry used at runtime.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct App {
|
||||
/// Whether this application is enabled.
|
||||
pub enable: bool,
|
||||
/// Display name of the application. Defaults to "App N" if not set.
|
||||
pub name: String,
|
||||
/// Icon to display for this application, if available.
|
||||
pub icon: Option<Icon>,
|
||||
/// Command to execute when the application is launched.
|
||||
pub command: UserCommand,
|
||||
/// Display format (icon, text, or both).
|
||||
pub display: ApplicationsDisplayFormat,
|
||||
/// Whether to show the launch command on hover.
|
||||
pub show_command_on_hover: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Renders the application button in the provided `Ui` context with a given icon size.
|
||||
#[inline]
|
||||
pub fn render(&mut self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||
if self.enable
|
||||
&& SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = Vec2::splat(4.0);
|
||||
|
||||
match self.display {
|
||||
ApplicationsDisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
|
||||
ApplicationsDisplayFormat::Text => self.draw_name(ui),
|
||||
ApplicationsDisplayFormat::IconAndText => {
|
||||
self.draw_icon(ctx, ui, icon_config);
|
||||
self.draw_name(ui);
|
||||
}
|
||||
}
|
||||
|
||||
// Add hover text with command information
|
||||
let response = ui.response();
|
||||
if self.show_command_on_hover {
|
||||
response.on_hover_text(format!("Launch: {}", self.command.as_ref()));
|
||||
}
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
// Launch the application when clicked
|
||||
self.command.launch_if_ready();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the application's icon within the UI if available,
|
||||
/// or falls back to a default placeholder icon.
|
||||
#[inline]
|
||||
fn draw_icon(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||
if let Some(icon) = &self.icon {
|
||||
icon.draw(ctx, ui, icon_config);
|
||||
} else {
|
||||
Icon::draw_fallback(ui, Vec2::splat(icon_config.size));
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays the application's name as a non-selectable label within the UI.
|
||||
#[inline]
|
||||
fn draw_name(&self, ui: &mut Ui) {
|
||||
ui.add(Label::new(&self.name).selectable(false));
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds image/text data to be used as an icon in the UI.
|
||||
/// This represents source icon data before rendering.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Icon {
|
||||
/// RGBA image used for rendering the icon.
|
||||
Image(ImageIcon),
|
||||
/// Text-based icon, e.g. from a font like Nerd Fonts.
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
/// Attempts to create an [`Icon`] from a string path or text glyph/glyphs.
|
||||
///
|
||||
/// - Environment variables in the path are resolved using [`PathExt::replace_env`].
|
||||
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved path.
|
||||
/// - If the path is invalid but the string is non-empty, it is interpreted as a text-based icon and
|
||||
/// returned as [`Icon::Text`].
|
||||
/// - Returns `None` if the input is empty, `None`, or image loading fails.
|
||||
#[inline]
|
||||
pub fn try_from_path(icon: Option<&str>) -> Option<Self> {
|
||||
let icon = icon.map(str::trim)?;
|
||||
if icon.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = icon.replace_env();
|
||||
if !path.is_file() {
|
||||
return Some(Icon::Text(icon.to_owned()));
|
||||
}
|
||||
|
||||
let image_icon = ImageIcon::try_load(path.as_ref(), || match image::open(&path) {
|
||||
Ok(img) => Some(img),
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to load icon from {:?}, error: {}", path, err);
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
Some(Icon::Image(image_icon))
|
||||
}
|
||||
|
||||
/// Attempts to create an [`Icon`] by extracting an image from the executable path of a [`UserCommand`].
|
||||
///
|
||||
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved executable path.
|
||||
/// - Returns [`Icon::Image`] if an icon is successfully extracted.
|
||||
/// - Returns `None` if the executable path is unavailable or icon extraction fails.
|
||||
#[inline]
|
||||
pub fn try_from_command(command: &UserCommand) -> Option<Self> {
|
||||
let path = command.get_executable()?;
|
||||
let image_icon = ImageIcon::try_load(path.as_ref(), || {
|
||||
let path_str = path.to_str()?;
|
||||
windows_icons::get_icon_by_path(path_str)
|
||||
.or_else(|| windows_icons_fallback::get_icon_by_path(path_str))
|
||||
})?;
|
||||
Some(Icon::Image(image_icon))
|
||||
}
|
||||
|
||||
/// Renders the icon in the given [`Ui`] using the provided [`IconConfig`].
|
||||
#[inline]
|
||||
pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
|
||||
match self {
|
||||
Icon::Image(image_icon) => {
|
||||
Frame::NONE
|
||||
.inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Image::from_texture(&image_icon.texture(ctx))
|
||||
.maintain_aspect_ratio(true)
|
||||
.fit_to_exact_size(Vec2::splat(icon_config.size)),
|
||||
);
|
||||
});
|
||||
}
|
||||
Icon::Text(icon) => {
|
||||
let rich_text = RichText::new(icon)
|
||||
.font(icon_config.font_id.clone())
|
||||
.size(icon_config.size)
|
||||
.color(icon_config.color);
|
||||
ui.add(Label::new(rich_text).selectable(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a fallback icon when the specified icon cannot be loaded.
|
||||
/// Displays a simple crossed-out rectangle as a placeholder.
|
||||
#[inline]
|
||||
pub fn draw_fallback(ui: &mut Ui, icon_size: Vec2) {
|
||||
let (response, painter) = ui.allocate_painter(icon_size, Sense::hover());
|
||||
let stroke = Stroke::new(1.0, ui.style().visuals.text_color());
|
||||
let mut rect = response.rect;
|
||||
let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
|
||||
rect = rect.shrink(stroke.width);
|
||||
let c = rect.center();
|
||||
let r = rect.width() / 2.0;
|
||||
painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
|
||||
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration structure for icon rendering
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IconConfig {
|
||||
/// Font used for text-based icons
|
||||
pub font_id: FontId,
|
||||
/// Size of the icon
|
||||
pub size: f32,
|
||||
/// Color of the icon used for text-based icons
|
||||
pub color: Color32,
|
||||
}
|
||||
|
||||
/// A structure to manage command execution with cooldown prevention.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserCommand {
|
||||
/// The command string to execute
|
||||
pub command: Arc<str>,
|
||||
/// Last time this command was executed (used for cooldown control)
|
||||
pub last_launch: Instant,
|
||||
}
|
||||
|
||||
impl AsRef<str> for UserCommand {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.command
|
||||
}
|
||||
}
|
||||
|
||||
impl UserCommand {
|
||||
/// Creates a new [`UserCommand`] with environment variables in the command path
|
||||
/// resolved using [`PathExt::replace_env`].
|
||||
#[inline]
|
||||
pub fn new(command: &str) -> Self {
|
||||
// Allow immediate launch by initializing last_launch in the past
|
||||
let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL;
|
||||
|
||||
Self {
|
||||
command: Arc::from(command.replace_env().to_str().unwrap_or_default()),
|
||||
last_launch,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to resolve the executable path from the command string.
|
||||
///
|
||||
/// Resolution logic:
|
||||
/// - Splits the command by ".exe" and checks if the first part is an existing file.
|
||||
/// - If not, attempts to locate the binary using [`which`] on this name.
|
||||
/// - If still unresolved, takes the first word (separated by whitespace) and attempts
|
||||
/// to find it in the system `PATH` using [`which`].
|
||||
///
|
||||
/// Returns `None` if no executable path can be determined.
|
||||
#[inline]
|
||||
pub fn get_executable(&self) -> Option<Cow<'_, Path>> {
|
||||
if let Some(binary) = self.command.split(".exe").next().map(Path::new) {
|
||||
if binary.is_file() {
|
||||
return Some(Cow::Borrowed(binary));
|
||||
} else if let Ok(binary) = which(binary) {
|
||||
return Some(Cow::Owned(binary));
|
||||
}
|
||||
}
|
||||
|
||||
which(self.command.split(' ').next()?).ok().map(Cow::Owned)
|
||||
}
|
||||
|
||||
/// Attempts to launch the specified command in a separate thread if enough time has passed
|
||||
/// since the last launch. This prevents repeated launches from rapid consecutive clicks.
|
||||
///
|
||||
/// Errors during launch are logged using the `tracing` crate.
|
||||
pub fn launch_if_ready(&mut self) {
|
||||
let now = Instant::now();
|
||||
// Check if enough time has passed since the last launch
|
||||
if now.duration_since(self.last_launch) < MIN_LAUNCH_INTERVAL {
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_launch = now;
|
||||
let command_string = self.command.clone();
|
||||
// Launch the application in a separate thread to avoid blocking the UI
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = Command::new("cmd").args(["/C", &command_string]).spawn() {
|
||||
tracing::error!("Failed to launch command '{}': {}", command_string, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,31 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use starship_battery::units::ratio::percent;
|
||||
use starship_battery::Manager;
|
||||
use starship_battery::State;
|
||||
use starship_battery::units::ratio::percent;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Battery widget configuration
|
||||
pub struct BatteryConfig {
|
||||
/// Enable the Battery widget
|
||||
pub enable: bool,
|
||||
/// Hide the widget if the battery is at full charge
|
||||
pub hide_on_full_charge: Option<bool>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
/// Data refresh interval in seconds
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
@@ -87,41 +89,41 @@ impl Battery {
|
||||
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
|
||||
output = None;
|
||||
|
||||
if let Ok(mut batteries) = self.manager.batteries() {
|
||||
if let Some(Ok(first)) = batteries.nth(0) {
|
||||
let percentage = first.state_of_charge().get::<percent>().round() as u8;
|
||||
if let Ok(mut batteries) = self.manager.batteries()
|
||||
&& let Some(Ok(first)) = batteries.nth(0)
|
||||
{
|
||||
let percentage = first.state_of_charge().get::<percent>().round() as u8;
|
||||
|
||||
if percentage == 100 && self.hide_on_full_charge {
|
||||
output = None
|
||||
} else {
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => {
|
||||
self.state = match percentage {
|
||||
p if p > 75 => BatteryState::Discharging,
|
||||
p if p > 50 => BatteryState::High,
|
||||
p if p > 25 => BatteryState::Medium,
|
||||
p if p > 10 => BatteryState::Low,
|
||||
_ => BatteryState::Warning,
|
||||
}
|
||||
if percentage == 100 && self.hide_on_full_charge {
|
||||
output = None
|
||||
} else {
|
||||
match first.state() {
|
||||
State::Charging => self.state = BatteryState::Charging,
|
||||
State::Discharging => {
|
||||
self.state = match percentage {
|
||||
p if p > 75 => BatteryState::Discharging,
|
||||
p if p > 50 => BatteryState::High,
|
||||
p if p > 25 => BatteryState::Medium,
|
||||
p if p > 10 => BatteryState::Low,
|
||||
_ => BatteryState::Warning,
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
|
||||
|
||||
output = Some(BatteryOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => {
|
||||
format!("{percentage}%")
|
||||
}
|
||||
},
|
||||
selected,
|
||||
})
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
|
||||
|
||||
output = Some(BatteryOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("BAT: {percentage}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => {
|
||||
format!("{percentage}%")
|
||||
}
|
||||
},
|
||||
selected,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,13 +178,11 @@ impl BarWidget for Battery {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
&& let Err(error) = Command::new("cmd.exe")
|
||||
.args(["/C", "start", "ms-settings:batterysaver"])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
{
|
||||
eprintln!("{error}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -18,10 +18,12 @@ use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// CPU widget configuration
|
||||
pub struct CpuConfig {
|
||||
/// Enable the Cpu widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
/// Data refresh interval in seconds
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
@@ -76,8 +78,8 @@ impl Cpu {
|
||||
|
||||
CpuOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {used}%"),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{used}%"),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
@@ -120,12 +122,10 @@ impl BarWidget for Cpu {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) =
|
||||
&& let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
{
|
||||
eprintln!("{error}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@ use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -61,6 +62,7 @@ impl CustomModifiers {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Date widget configuration
|
||||
pub struct DateConfig {
|
||||
/// Enable the Date widget
|
||||
pub enable: bool,
|
||||
@@ -103,6 +105,7 @@ impl From<DateConfig> for Date {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Date widget format
|
||||
pub enum DateFormat {
|
||||
/// Month/Date/Year format (09/08/24)
|
||||
MonthDateYear,
|
||||
@@ -113,8 +116,10 @@ pub enum DateFormat {
|
||||
/// Day Date Month Year format (8 September 2024)
|
||||
DayDateMonthYear,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
Custom(String),
|
||||
/// Custom format with modifiers
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "CustomModifiers"))]
|
||||
CustomModifiers(CustomModifiers),
|
||||
}
|
||||
|
||||
@@ -166,7 +171,7 @@ impl Date {
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
Err(_) => format!("Invalid timezone: {}", timezone),
|
||||
Err(_) => format!("Invalid timezone: {timezone}"),
|
||||
},
|
||||
None => Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
@@ -225,7 +230,7 @@ impl BarWidget for Date {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(WidgetText::LayoutJob(layout_job.clone()))
|
||||
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
|
||||
.selectable(false),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use color_eyre::eyre;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::WidgetText;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use windows::Win32::Globalization::LCIDToLocaleName;
|
||||
@@ -19,15 +21,16 @@ use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
|
||||
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
|
||||
const ERROR_TEXT: &str = "Error";
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Keyboard widget configuration
|
||||
pub struct KeyboardConfig {
|
||||
/// Enable the Input widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 1 second)
|
||||
/// Data refresh interval
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
@@ -35,9 +38,7 @@ pub struct KeyboardConfig {
|
||||
|
||||
impl From<KeyboardConfig> for Keyboard {
|
||||
fn from(value: KeyboardConfig) -> Self {
|
||||
let data_refresh_interval = value
|
||||
.data_refresh_interval
|
||||
.unwrap_or(DEFAULT_DATA_REFRESH_INTERVAL);
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
@@ -80,7 +81,7 @@ pub struct Keyboard {
|
||||
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
|
||||
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
|
||||
/// invalid UTF-16 characters during conversion.
|
||||
fn get_active_keyboard_layout() -> Result<String, ()> {
|
||||
fn get_active_keyboard_layout() -> eyre::Result<String, ()> {
|
||||
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
|
||||
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
|
||||
|
||||
@@ -169,7 +170,10 @@ impl BarWidget for Keyboard {
|
||||
);
|
||||
|
||||
config.apply_on_widget(true, ui, |ui| {
|
||||
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
|
||||
ui.add(
|
||||
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
|
||||
.selectable(false),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ use crate::config::DisplayFormat;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::komorebi::KomorebiLayoutConfig;
|
||||
use eframe::egui::vec2;
|
||||
use color_eyre::eyre;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
use eframe::egui::FontId;
|
||||
@@ -13,11 +13,12 @@ use eframe::egui::Stroke;
|
||||
use eframe::egui::StrokeKind;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::vec2;
|
||||
use komorebi_client::SocketMessage;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::de::Error;
|
||||
use serde_json::from_str;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
@@ -25,24 +26,30 @@ use std::fmt::Formatter;
|
||||
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Komorebi layout kind
|
||||
pub enum KomorebiLayout {
|
||||
/// Predefined layout
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Default"))]
|
||||
Default(komorebi_client::DefaultLayout),
|
||||
/// Monocle mode
|
||||
Monocle,
|
||||
/// Floating layer
|
||||
Floating,
|
||||
/// Paused
|
||||
Paused,
|
||||
/// Custom layout
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
fn deserialize<D>(deserializer: D) -> eyre::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: String = String::deserialize(deserializer)?;
|
||||
|
||||
// Attempt to deserialize the string as a DefaultLayout
|
||||
if let Ok(default_layout) =
|
||||
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
|
||||
if let Ok(default_layout) = from_str::<komorebi_client::DefaultLayout>(&format!("\"{s}\""))
|
||||
{
|
||||
return Ok(KomorebiLayout::Default(default_layout));
|
||||
}
|
||||
@@ -53,7 +60,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
"Floating" => Ok(KomorebiLayout::Floating),
|
||||
"Paused" => Ok(KomorebiLayout::Paused),
|
||||
"Custom" => Ok(KomorebiLayout::Custom),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,16 +99,15 @@ impl KomorebiLayout {
|
||||
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
|
||||
match self {
|
||||
KomorebiLayout::Default(option) => {
|
||||
if let Some(ws_idx) = workspace_idx {
|
||||
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
||||
if let Some(ws_idx) = workspace_idx
|
||||
&& komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
|
||||
monitor_idx,
|
||||
ws_idx,
|
||||
*option,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
||||
}
|
||||
{
|
||||
tracing::error!("could not send message to komorebi: WorkspaceLayout");
|
||||
}
|
||||
}
|
||||
KomorebiLayout::Monocle => {
|
||||
@@ -188,6 +194,12 @@ impl KomorebiLayout {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
// TODO: @CtByte can you think of a nice icon to draw here?
|
||||
komorebi_client::DefaultLayout::Scrolling => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
},
|
||||
KomorebiLayout::Monocle => {}
|
||||
KomorebiLayout::Floating => {
|
||||
@@ -264,57 +276,53 @@ impl KomorebiLayout {
|
||||
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
|
||||
}
|
||||
|
||||
if show_options {
|
||||
if let Some(workspace_idx) = workspace_idx {
|
||||
Frame::NONE.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||
.selectable(false),
|
||||
);
|
||||
if show_options && let Some(workspace_idx) = workspace_idx {
|
||||
Frame::NONE.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
|
||||
.selectable(false),
|
||||
);
|
||||
|
||||
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::HorizontalStack,
|
||||
),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
||||
//KomorebiLayout::Custom,
|
||||
KomorebiLayout::Monocle,
|
||||
KomorebiLayout::Floating,
|
||||
KomorebiLayout::Paused,
|
||||
]);
|
||||
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::RightMainVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::HorizontalStack),
|
||||
KomorebiLayout::Default(
|
||||
komorebi_client::DefaultLayout::UltrawideVerticalStack,
|
||||
),
|
||||
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
|
||||
//KomorebiLayout::Custom,
|
||||
KomorebiLayout::Monocle,
|
||||
KomorebiLayout::Floating,
|
||||
KomorebiLayout::Paused,
|
||||
]);
|
||||
|
||||
for layout_option in &mut layout_options {
|
||||
let is_selected = self == layout_option;
|
||||
for layout_option in &mut layout_options {
|
||||
let is_selected = self == layout_option;
|
||||
|
||||
if SelectableFrame::new(is_selected)
|
||||
.show(ui, |ui| {
|
||||
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
|
||||
})
|
||||
.on_hover_text(match layout_option {
|
||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
||||
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
||||
KomorebiLayout::Custom => "Custom".to_string(),
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
||||
show_options = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
if SelectableFrame::new(is_selected)
|
||||
.show(ui, |ui| {
|
||||
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
|
||||
})
|
||||
.on_hover_text(match layout_option {
|
||||
KomorebiLayout::Default(layout) => layout.to_string(),
|
||||
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
|
||||
KomorebiLayout::Floating => "Toggle tiling".to_string(),
|
||||
KomorebiLayout::Paused => "Toggle pause".to_string(),
|
||||
KomorebiLayout::Custom => "Custom".to_string(),
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
|
||||
show_options = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -17,6 +17,7 @@ use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Media widget configuration
|
||||
pub struct MediaConfig {
|
||||
/// Enable the Media widget
|
||||
pub enable: bool,
|
||||
@@ -40,36 +41,34 @@ impl Media {
|
||||
enable,
|
||||
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
|
||||
.unwrap()
|
||||
.get()
|
||||
.join()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession() {
|
||||
if let Ok(op) = session.TryTogglePlayPauseAsync() {
|
||||
op.get().unwrap_or_default();
|
||||
}
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(op) = session.TryTogglePlayPauseAsync()
|
||||
{
|
||||
op.join().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}");
|
||||
}
|
||||
}
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
|
||||
&& let Ok(properties) = operation.join()
|
||||
&& 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()
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -18,10 +18,12 @@ use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Memory widget configuration
|
||||
pub struct MemoryConfig {
|
||||
/// Enable the Memory widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
/// Data refresh interval in seconds
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
@@ -79,9 +81,9 @@ impl Memory {
|
||||
MemoryOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", usage)
|
||||
format!("RAM: {usage}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{usage}%"),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
@@ -124,12 +126,10 @@ impl BarWidget for Memory {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) =
|
||||
&& let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
{
|
||||
eprintln!("{error}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
use eframe::egui::ColorImage;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::TextureHandle;
|
||||
use eframe::egui::TextureOptions;
|
||||
use image::RgbaImage;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::RwLock;
|
||||
|
||||
pub mod applications;
|
||||
pub mod battery;
|
||||
pub mod cpu;
|
||||
pub mod date;
|
||||
@@ -11,3 +23,151 @@ pub mod storage;
|
||||
pub mod time;
|
||||
pub mod update;
|
||||
pub mod widget;
|
||||
|
||||
/// Global cache for icon images and their associated GPU textures.
|
||||
pub static ICONS_CACHE: IconsCache = IconsCache::new();
|
||||
|
||||
/// In-memory cache for icon images and their associated GPU textures.
|
||||
///
|
||||
/// Stores raw [`ColorImage`]s and [`TextureHandle`]s keyed by [`ImageIconId`].
|
||||
/// Texture entries are context-dependent and automatically invalidated when the [`Context`] changes.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct IconsCache {
|
||||
textures: LazyLock<RwLock<(Option<Context>, HashMap<ImageIconId, TextureHandle>)>>,
|
||||
images: LazyLock<RwLock<HashMap<ImageIconId, Arc<ColorImage>>>>,
|
||||
}
|
||||
|
||||
impl IconsCache {
|
||||
/// Creates a new empty IconsCache instance.
|
||||
#[inline]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
textures: LazyLock::new(|| RwLock::new((None, HashMap::new()))),
|
||||
images: LazyLock::new(|| RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves or creates a texture handle for the given icon ID and image.
|
||||
///
|
||||
/// If a texture for the given ID already exists for the current [`Context`], it is reused.
|
||||
/// Otherwise, a new texture is created, inserted into the cache, and returned.
|
||||
/// The cache is reset if the [`Context`] has changed.
|
||||
#[inline]
|
||||
pub fn texture(&self, ctx: &Context, id: &ImageIconId, img: &Arc<ColorImage>) -> TextureHandle {
|
||||
if let Some(texture) = self.get_texture(ctx, id) {
|
||||
return texture;
|
||||
}
|
||||
let texture_handle = ctx.load_texture("icon", img.clone(), TextureOptions::default());
|
||||
self.insert_texture(ctx, id.clone(), texture_handle.clone());
|
||||
texture_handle
|
||||
}
|
||||
|
||||
/// Returns the cached texture for the given icon ID if it exists and matches the current [`Context`].
|
||||
pub fn get_texture(&self, ctx: &Context, id: &ImageIconId) -> Option<TextureHandle> {
|
||||
let textures_lock = self.textures.read().unwrap();
|
||||
if textures_lock.0.as_ref() == Some(ctx) {
|
||||
return textures_lock.1.get(id).cloned();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Inserts a texture handle, resetting the cache if the [`Context`] has changed.
|
||||
pub fn insert_texture(&self, ctx: &Context, id: ImageIconId, texture: TextureHandle) {
|
||||
let mut textures_lock = self.textures.write().unwrap();
|
||||
|
||||
if textures_lock.0.as_ref() != Some(ctx) {
|
||||
textures_lock.0 = Some(ctx.clone());
|
||||
textures_lock.1.clear();
|
||||
}
|
||||
|
||||
textures_lock.1.insert(id, texture);
|
||||
}
|
||||
|
||||
/// Returns the cached image for the given icon ID, if available.
|
||||
pub fn get_image(&self, id: &ImageIconId) -> Option<Arc<ColorImage>> {
|
||||
self.images.read().unwrap().get(id).cloned()
|
||||
}
|
||||
|
||||
/// Caches a raw [`ColorImage`] associated with the given icon ID.
|
||||
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
|
||||
self.images.write().unwrap().insert(id, image);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
|
||||
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
|
||||
let pixels = rgba_image.as_flat_samples();
|
||||
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
|
||||
}
|
||||
|
||||
/// Represents an image-based icon with a unique ID and pixel data.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImageIcon {
|
||||
/// Unique identifier for the image icon, used for texture caching.
|
||||
pub id: ImageIconId,
|
||||
/// Shared pixel data of the icon in `ColorImage` format.
|
||||
pub image: Arc<ColorImage>,
|
||||
}
|
||||
|
||||
impl ImageIcon {
|
||||
/// Creates a new [`ImageIcon`] from the given ID and image data.
|
||||
#[inline]
|
||||
pub fn new(id: ImageIconId, image: Arc<ColorImage>) -> Self {
|
||||
Self { id, image }
|
||||
}
|
||||
|
||||
/// Loads an [`ImageIcon`] from [`ICONS_CACHE`] or calls `loader` if not cached.
|
||||
/// The loaded image is converted to a [`ColorImage`], cached, and returned.
|
||||
#[inline]
|
||||
pub fn try_load<F, I>(id: impl Into<ImageIconId>, loader: F) -> Option<Self>
|
||||
where
|
||||
F: FnOnce() -> Option<I>,
|
||||
I: Into<RgbaImage>,
|
||||
{
|
||||
let id = id.into();
|
||||
let image = ICONS_CACHE.get_image(&id).or_else(|| {
|
||||
let img = loader()?;
|
||||
let img = Arc::new(rgba_to_color_image(&img.into()));
|
||||
ICONS_CACHE.insert_image(id.clone(), img.clone());
|
||||
Some(img)
|
||||
})?;
|
||||
|
||||
Some(ImageIcon::new(id, image))
|
||||
}
|
||||
|
||||
/// Returns a texture handle for the icon, using the given [`Context`].
|
||||
///
|
||||
/// If the texture is already cached in [`ICONS_CACHE`], it is reused.
|
||||
/// Otherwise, a new texture is created from the [`ColorImage`] and cached.
|
||||
#[inline]
|
||||
pub fn texture(&self, ctx: &Context) -> TextureHandle {
|
||||
ICONS_CACHE.texture(ctx, &self.id, &self.image)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unique identifier for an image-based icon.
|
||||
///
|
||||
/// Used to distinguish cached images and textures by either a file path
|
||||
/// or a Windows window handle.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum ImageIconId {
|
||||
/// Identifier based on a file system path.
|
||||
Path(Arc<Path>),
|
||||
/// Windows HWND handle.
|
||||
Hwnd(isize),
|
||||
}
|
||||
|
||||
impl From<&Path> for ImageIconId {
|
||||
#[inline]
|
||||
fn from(value: &Path) -> Self {
|
||||
Self::Path(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<isize> for ImageIconId {
|
||||
#[inline]
|
||||
fn from(value: isize) -> Self {
|
||||
Self::Hwnd(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,30 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use num_derive::FromPrimitive;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Networks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Network widget configuration
|
||||
pub struct NetworkConfig {
|
||||
/// Enable the Network widget
|
||||
pub enable: bool,
|
||||
@@ -35,7 +41,8 @@ pub struct NetworkConfig {
|
||||
/// Characters to reserve for received and transmitted activity
|
||||
#[serde(alias = "network_activity_fill_characters")]
|
||||
pub activity_left_padding: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
/// Data refresh interval in seconds
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
@@ -45,6 +52,7 @@ pub struct NetworkConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Network select configuration
|
||||
pub struct NetworkSelectConfig {
|
||||
/// Select the total received data when it's over this value
|
||||
pub total_received_over: Option<u64>,
|
||||
@@ -58,24 +66,35 @@ pub struct NetworkSelectConfig {
|
||||
|
||||
impl From<NetworkConfig> for Network {
|
||||
fn from(value: NetworkConfig) -> Self {
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
let default_refresh_interval = 10;
|
||||
let data_refresh_interval = value
|
||||
.data_refresh_interval
|
||||
.unwrap_or(default_refresh_interval);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
show_total_activity: value.show_total_activity,
|
||||
show_activity: value.show_activity,
|
||||
show_default_interface: value.show_default_interface.unwrap_or(true),
|
||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
||||
default_interface: String::new(),
|
||||
networks_network_activity: Arc::new(Mutex::new(Networks::new_with_refreshed_list())),
|
||||
default_interface: Arc::new(Mutex::new(String::new())),
|
||||
interface_generation: Arc::new(AtomicU64::new(0)),
|
||||
default_refresh_interval,
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
auto_select: value.auto_select,
|
||||
activity_left_padding: value.activity_left_padding.unwrap_or_default(),
|
||||
last_state_total_activity: vec![],
|
||||
last_state_activity: vec![],
|
||||
last_updated_network_activity: Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
last_update_request_default_interface: Instant::now()
|
||||
.checked_sub(Duration::from_secs(default_refresh_interval))
|
||||
.unwrap(),
|
||||
last_state_total_activity: Arc::new(Mutex::new(vec![])),
|
||||
last_state_activity: Arc::new(Mutex::new(vec![])),
|
||||
last_update_request_network_activity: Arc::new(Mutex::new(
|
||||
Instant::now()
|
||||
.checked_sub(Duration::from_secs(data_refresh_interval))
|
||||
.unwrap(),
|
||||
)),
|
||||
activity_generation: Arc::new(AtomicU64::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,84 +104,139 @@ pub struct Network {
|
||||
pub show_total_activity: bool,
|
||||
pub show_activity: bool,
|
||||
pub show_default_interface: bool,
|
||||
networks_network_activity: Networks,
|
||||
networks_network_activity: Arc<Mutex<Networks>>,
|
||||
default_refresh_interval: u64,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select: Option<NetworkSelectConfig>,
|
||||
default_interface: String,
|
||||
last_state_total_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
last_updated_network_activity: Instant,
|
||||
default_interface: Arc<Mutex<String>>,
|
||||
interface_generation: Arc<AtomicU64>,
|
||||
last_update_request_default_interface: Instant,
|
||||
activity_generation: Arc<AtomicU64>,
|
||||
last_state_activity: Arc<Mutex<Vec<NetworkReading>>>,
|
||||
last_state_total_activity: Arc<Mutex<Vec<NetworkReading>>>,
|
||||
last_update_request_network_activity: Arc<Mutex<Instant>>,
|
||||
activity_left_padding: 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 update_default_interface_async(&mut self) {
|
||||
let gen_ = self.interface_generation.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
let gen_arc = Arc::clone(&self.interface_generation);
|
||||
let iface_arc = Arc::clone(&self.default_interface);
|
||||
|
||||
thread::spawn(move || {
|
||||
if let Ok(interface) = netdev::get_default_interface()
|
||||
&& let Some(friendly_name) = &interface.friendly_name
|
||||
{
|
||||
// Only update if this is the latest request
|
||||
if gen_ == gen_arc.load(Ordering::SeqCst)
|
||||
&& let Ok(mut iface) = iface_arc.lock()
|
||||
{
|
||||
*iface = friendly_name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let mut activity = self.last_state_activity.clone();
|
||||
let mut total_activity = self.last_state_total_activity.clone();
|
||||
fn default_interface(&mut self) -> String {
|
||||
let current = self.default_interface.lock().unwrap().clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
if now.duration_since(self.last_update_request_default_interface)
|
||||
> Duration::from_secs(self.default_refresh_interval)
|
||||
{
|
||||
activity.clear();
|
||||
total_activity.clear();
|
||||
self.last_update_request_default_interface = now;
|
||||
self.update_default_interface_async();
|
||||
}
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface() {
|
||||
if let Some(friendly_name) = &interface.friendly_name {
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
current
|
||||
}
|
||||
|
||||
self.networks_network_activity.refresh(true);
|
||||
fn update_network_activity_async(&mut self) {
|
||||
let gen_ = self.activity_generation.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
let gen_arc = Arc::clone(&self.activity_generation);
|
||||
let activity_arc = Arc::clone(&self.last_state_activity);
|
||||
let total_activity_arc = Arc::clone(&self.last_state_total_activity);
|
||||
let data_refresh_interval = self.data_refresh_interval;
|
||||
let show_activity = self.show_activity;
|
||||
let show_total_activity = self.show_total_activity;
|
||||
let networks_network_activity_arc = Arc::clone(&self.networks_network_activity);
|
||||
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if self.show_activity {
|
||||
let received = Self::to_pretty_bytes(
|
||||
data.received(),
|
||||
self.data_refresh_interval,
|
||||
);
|
||||
let transmitted = Self::to_pretty_bytes(
|
||||
data.transmitted(),
|
||||
self.data_refresh_interval,
|
||||
);
|
||||
thread::spawn(move || {
|
||||
let mut activity = Vec::new();
|
||||
let mut total_activity = Vec::new();
|
||||
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
ReadingValue::from(received),
|
||||
ReadingValue::from(transmitted),
|
||||
));
|
||||
}
|
||||
if let Ok(interface) = netdev::get_default_interface()
|
||||
&& let Some(friendly_name) = &interface.friendly_name
|
||||
&& let Ok(mut networks) = networks_network_activity_arc.lock()
|
||||
{
|
||||
networks.refresh(true);
|
||||
|
||||
if self.show_total_activity {
|
||||
let total_received =
|
||||
Self::to_pretty_bytes(data.total_received(), 1);
|
||||
let total_transmitted =
|
||||
Self::to_pretty_bytes(data.total_transmitted(), 1);
|
||||
for (interface_name, data) in &*networks {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if show_activity {
|
||||
let received =
|
||||
Network::to_pretty_bytes(data.received(), data_refresh_interval);
|
||||
let transmitted =
|
||||
Network::to_pretty_bytes(data.transmitted(), data_refresh_interval);
|
||||
|
||||
total_activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Total,
|
||||
ReadingValue::from(total_received),
|
||||
ReadingValue::from(total_transmitted),
|
||||
))
|
||||
}
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
ReadingValue::from(received),
|
||||
ReadingValue::from(transmitted),
|
||||
));
|
||||
}
|
||||
|
||||
if show_total_activity {
|
||||
let total_received = Network::to_pretty_bytes(data.total_received(), 1);
|
||||
let total_transmitted =
|
||||
Network::to_pretty_bytes(data.total_transmitted(), 1);
|
||||
|
||||
total_activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Total,
|
||||
ReadingValue::from(total_received),
|
||||
ReadingValue::from(total_transmitted),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_activity.clone_from(&activity);
|
||||
self.last_state_total_activity.clone_from(&total_activity);
|
||||
self.last_updated_network_activity = now;
|
||||
// Only update if this is the latest request
|
||||
if gen_ == gen_arc.load(Ordering::SeqCst) {
|
||||
if let Ok(mut act) = activity_arc.lock() {
|
||||
*act = activity;
|
||||
}
|
||||
if let Ok(mut tot) = total_activity_arc.lock() {
|
||||
*tot = total_activity;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let now = Instant::now();
|
||||
let should_update = {
|
||||
let last_update_request = self.last_update_request_network_activity.lock().unwrap();
|
||||
now.duration_since(*last_update_request)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
};
|
||||
|
||||
if should_update {
|
||||
{
|
||||
let mut last_updated = self.last_update_request_network_activity.lock().unwrap();
|
||||
*last_updated = now;
|
||||
}
|
||||
self.update_network_activity_async();
|
||||
}
|
||||
|
||||
self.get_network_activity()
|
||||
}
|
||||
|
||||
fn get_network_activity(&self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let activity = self.last_state_activity.lock().unwrap().clone();
|
||||
let total_activity = self.last_state_total_activity.lock().unwrap().clone();
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
@@ -312,10 +386,9 @@ impl Network {
|
||||
if SelectableFrame::new_auto(selected, auto_focus_fill)
|
||||
.show(ui, add_contents)
|
||||
.clicked()
|
||||
&& let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
||||
eprintln!("{}", error);
|
||||
}
|
||||
eprintln!("{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,9 +507,9 @@ impl BarWidget for Network {
|
||||
}
|
||||
|
||||
if self.show_default_interface {
|
||||
self.default_interface();
|
||||
let mut self_default_interface = self.default_interface();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
if !self_default_interface.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
@@ -450,11 +523,11 @@ impl BarWidget for Network {
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
self.default_interface.insert_str(0, "NET: ");
|
||||
self_default_interface.insert_str(0, "NET: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&self.default_interface,
|
||||
&self_default_interface,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
@@ -535,6 +608,6 @@ enum DataUnit {
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -18,13 +18,21 @@ use sysinfo::Disks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Storage widget configuration
|
||||
pub struct StorageConfig {
|
||||
/// Enable the Storage widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
/// Data refresh interval in seconds
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Show disks that are read only
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
|
||||
pub show_read_only_disks: Option<bool>,
|
||||
/// Show removable disks
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||
pub show_removable_disks: Option<bool>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
/// Hide when the current percentage is under this value [[1-100]]
|
||||
@@ -38,6 +46,8 @@ impl From<StorageConfig> for Storage {
|
||||
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),
|
||||
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
|
||||
show_removable_disks: value.show_removable_disks.unwrap_or(true),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now(),
|
||||
@@ -55,6 +65,8 @@ pub struct Storage {
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
show_read_only_disks: bool,
|
||||
show_removable_disks: bool,
|
||||
auto_select_over: Option<u8>,
|
||||
auto_hide_under: Option<u8>,
|
||||
last_updated: Instant,
|
||||
@@ -71,6 +83,12 @@ impl Storage {
|
||||
let mut disks = vec![];
|
||||
|
||||
for disk in &self.disks {
|
||||
if disk.is_read_only() && !self.show_read_only_disks {
|
||||
continue;
|
||||
}
|
||||
if disk.is_removable() && !self.show_removable_disks {
|
||||
continue;
|
||||
}
|
||||
let mount = disk.mount_point();
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
@@ -87,7 +105,7 @@ impl Storage {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), percentage)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
|
||||
},
|
||||
selected,
|
||||
})
|
||||
@@ -142,17 +160,15 @@ impl BarWidget for Storage {
|
||||
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe")
|
||||
&& let Err(error) = Command::new("cmd.exe")
|
||||
.args([
|
||||
"/C",
|
||||
"explorer.exe",
|
||||
output.label.split(' ').collect::<Vec<&str>>()[0],
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
{
|
||||
eprintln!("{error}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::widgets::widget::BarWidget;
|
||||
use chrono::Local;
|
||||
use chrono::NaiveTime;
|
||||
use chrono_tz::Tz;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::CornerRadius;
|
||||
@@ -16,6 +15,7 @@ use eframe::egui::Stroke;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::Vec2;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::epaint::StrokeKind;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
@@ -72,6 +72,7 @@ lazy_static! {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Time widget configuration
|
||||
pub struct TimeConfig {
|
||||
/// Enable the Time widget
|
||||
pub enable: bool,
|
||||
@@ -92,7 +93,7 @@ pub struct TimeConfig {
|
||||
///}
|
||||
/// ```
|
||||
pub timezone: Option<String>,
|
||||
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00. (default: false)
|
||||
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00
|
||||
pub changing_icon: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -119,6 +120,7 @@ impl From<TimeConfig> for Time {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Time format
|
||||
pub enum TimeFormat {
|
||||
/// Twelve-hour format (with seconds)
|
||||
TwelveHour,
|
||||
@@ -133,6 +135,7 @@ pub enum TimeFormat {
|
||||
/// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
|
||||
BinaryRectangle,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
@@ -209,7 +212,7 @@ impl Time {
|
||||
Some(dt.time()),
|
||||
)
|
||||
}
|
||||
Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
|
||||
Err(_) => (format!("Invalid timezone: {timezone:?}"), None),
|
||||
},
|
||||
None => {
|
||||
let dt = Local::now();
|
||||
|
||||
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use eframe::egui::Align;
|
||||
use eframe::egui::Context;
|
||||
use eframe::egui::Label;
|
||||
use eframe::egui::TextFormat;
|
||||
use eframe::egui::Ui;
|
||||
use eframe::egui::text::LayoutJob;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
@@ -16,10 +16,12 @@ use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Update widget configuration
|
||||
pub struct UpdateConfig {
|
||||
/// Enable the Update widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 12 hours)
|
||||
/// Data refresh interval in hours
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
@@ -140,16 +142,14 @@ impl BarWidget for Update {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("explorer.exe")
|
||||
&& let Err(error) = Command::new("explorer.exe")
|
||||
.args([format!(
|
||||
"https://github.com/LGUG2Z/komorebi/releases/v{}",
|
||||
self.latest_version
|
||||
)])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
}
|
||||
{
|
||||
eprintln!("{error}")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::render::RenderConfig;
|
||||
use crate::widgets::applications::Applications;
|
||||
use crate::widgets::applications::ApplicationsConfig;
|
||||
use crate::widgets::battery::Battery;
|
||||
use crate::widgets::battery::BatteryConfig;
|
||||
use crate::widgets::cpu::Cpu;
|
||||
@@ -32,23 +34,50 @@ pub trait BarWidget {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Widget configuration
|
||||
pub enum WidgetConfig {
|
||||
/// Applications widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Applications"))]
|
||||
Applications(ApplicationsConfig),
|
||||
/// Battery widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Battery"))]
|
||||
Battery(BatteryConfig),
|
||||
/// CPU widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Cpu"))]
|
||||
Cpu(CpuConfig),
|
||||
/// Date widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Date"))]
|
||||
Date(DateConfig),
|
||||
/// Keyboard widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Keyboard"))]
|
||||
Keyboard(KeyboardConfig),
|
||||
/// Komorebi widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Komorebi"))]
|
||||
Komorebi(KomorebiConfig),
|
||||
/// Media widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Media"))]
|
||||
Media(MediaConfig),
|
||||
/// Memory widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Memory"))]
|
||||
Memory(MemoryConfig),
|
||||
/// Network widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Network"))]
|
||||
Network(NetworkConfig),
|
||||
/// Storage widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
|
||||
Storage(StorageConfig),
|
||||
/// Time widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
|
||||
Time(TimeConfig),
|
||||
/// Update widget configuration
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Update"))]
|
||||
Update(UpdateConfig),
|
||||
}
|
||||
|
||||
impl WidgetConfig {
|
||||
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
|
||||
match self {
|
||||
WidgetConfig::Applications(config) => Box::new(Applications::from(config)),
|
||||
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())),
|
||||
@@ -65,6 +94,7 @@ impl WidgetConfig {
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
match self {
|
||||
WidgetConfig::Applications(config) => config.enable,
|
||||
WidgetConfig::Battery(config) => config.enable,
|
||||
WidgetConfig::Cpu(config) => config.enable,
|
||||
WidgetConfig::Date(config) => config.enable,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi = { path = "../komorebi" }
|
||||
komorebi = { path = "../komorebi", default-features = false }
|
||||
|
||||
uds_windows = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
schemars = ["komorebi/schemars"]
|
||||
schemars = ["komorebi/default"]
|
||||
|
||||
@@ -1,8 +1,36 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
pub use komorebi::animation::prefix::AnimationPrefix;
|
||||
pub use komorebi::AnimationsConfig;
|
||||
pub use komorebi::AppSpecificConfigurationPath;
|
||||
pub use komorebi::AspectRatio;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::Colour;
|
||||
pub use komorebi::CrossBoundaryBehaviour;
|
||||
pub use komorebi::GridLayoutOptions;
|
||||
pub use komorebi::KomorebiTheme;
|
||||
pub use komorebi::LayoutOptions;
|
||||
pub use komorebi::MonitorConfig;
|
||||
pub use komorebi::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::Placement;
|
||||
pub use komorebi::PredefinedAspectRatio;
|
||||
pub use komorebi::Rgb;
|
||||
pub use komorebi::RuleDebug;
|
||||
pub use komorebi::ScrollingLayoutOptions;
|
||||
pub use komorebi::StackbarConfig;
|
||||
pub use komorebi::StaticConfig;
|
||||
pub use komorebi::SubscribeOptions;
|
||||
pub use komorebi::TabsConfig;
|
||||
pub use komorebi::ThemeOptions;
|
||||
pub use komorebi::VirtualDesktopNotification;
|
||||
pub use komorebi::Wallpaper;
|
||||
pub use komorebi::WindowContainerBehaviour;
|
||||
pub use komorebi::WindowHandlingBehaviour;
|
||||
pub use komorebi::WindowsApi;
|
||||
pub use komorebi::WorkspaceConfig;
|
||||
pub use komorebi::animation::PerAnimationPrefixConfig;
|
||||
pub use komorebi::animation::prefix::AnimationPrefix;
|
||||
pub use komorebi::asc::ApplicationSpecificConfiguration;
|
||||
pub use komorebi::border_manager::BorderInfo;
|
||||
pub use komorebi::config_generation::ApplicationConfiguration;
|
||||
@@ -11,8 +39,6 @@ pub use komorebi::config_generation::IdWithIdentifierAndComment;
|
||||
pub use komorebi::config_generation::MatchingRule;
|
||||
pub use komorebi::config_generation::MatchingStrategy;
|
||||
pub use komorebi::container::Container;
|
||||
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
||||
pub use komorebi::core::replace_env_in_path;
|
||||
pub use komorebi::core::AnimationStyle;
|
||||
pub use komorebi::core::ApplicationIdentifier;
|
||||
pub use komorebi::core::Arrangement;
|
||||
@@ -42,40 +68,24 @@ pub use komorebi::core::StackbarLabel;
|
||||
pub use komorebi::core::StackbarMode;
|
||||
pub use komorebi::core::StateQuery;
|
||||
pub use komorebi::core::WindowKind;
|
||||
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
|
||||
pub use komorebi::core::replace_env_in_path;
|
||||
pub use komorebi::monitor::Monitor;
|
||||
pub use komorebi::monitor_reconciliator::MonitorNotification;
|
||||
pub use komorebi::ring::Ring;
|
||||
pub use komorebi::splash;
|
||||
pub use komorebi::state::GlobalState;
|
||||
pub use komorebi::state::State;
|
||||
pub use komorebi::win32_display_data;
|
||||
pub use komorebi::window::Window;
|
||||
pub use komorebi::window_manager_event::WindowManagerEvent;
|
||||
pub use komorebi::workspace::Workspace;
|
||||
pub use komorebi::workspace::WorkspaceGlobals;
|
||||
pub use komorebi::workspace::WorkspaceLayer;
|
||||
pub use komorebi::AnimationsConfig;
|
||||
pub use komorebi::AppSpecificConfigurationPath;
|
||||
pub use komorebi::AspectRatio;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::Colour;
|
||||
pub use komorebi::CrossBoundaryBehaviour;
|
||||
pub use komorebi::GlobalState;
|
||||
pub use komorebi::KomorebiTheme;
|
||||
pub use komorebi::MonitorConfig;
|
||||
pub use komorebi::Notification;
|
||||
pub use komorebi::NotificationEvent;
|
||||
pub use komorebi::PredefinedAspectRatio;
|
||||
pub use komorebi::Rgb;
|
||||
pub use komorebi::RuleDebug;
|
||||
pub use komorebi::StackbarConfig;
|
||||
pub use komorebi::State;
|
||||
pub use komorebi::StaticConfig;
|
||||
pub use komorebi::SubscribeOptions;
|
||||
pub use komorebi::TabsConfig;
|
||||
pub use komorebi::WindowContainerBehaviour;
|
||||
pub use komorebi::WindowsApi;
|
||||
pub use komorebi::WorkspaceConfig;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
@@ -93,12 +103,15 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
|
||||
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()>
|
||||
where
|
||||
Q: Borrow<SocketMessage>,
|
||||
{
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
|
||||
if let Ok(m_str) = serde_json::to_string(&m) {
|
||||
if let Ok(m_str) = serde_json::to_string(m.borrow()) {
|
||||
s.push_str(&m_str);
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-client = { path = "../komorebi-client" }
|
||||
komorebi-client = { path = "../komorebi-client", default-features = false }
|
||||
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
random_word = { version = "0.5", features = ["en"] }
|
||||
serde_json = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
use eframe::egui;
|
||||
use eframe::egui::color_picker::Alpha;
|
||||
use eframe::egui::Color32;
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use eframe::egui::color_picker::Alpha;
|
||||
use komorebi_client::BorderStyle;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::DefaultLayout;
|
||||
@@ -78,8 +78,8 @@ impl From<&komorebi_client::Monitor> for MonitorConfig {
|
||||
}
|
||||
|
||||
Self {
|
||||
size: *value.size(),
|
||||
work_area_offset: value.work_area_offset().unwrap_or_default(),
|
||||
size: value.size,
|
||||
work_area_offset: value.work_area_offset.unwrap_or_default(),
|
||||
workspaces,
|
||||
}
|
||||
}
|
||||
@@ -95,22 +95,22 @@ struct WorkspaceConfig {
|
||||
|
||||
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
|
||||
fn from(value: &komorebi_client::Workspace) -> Self {
|
||||
let layout = match value.layout() {
|
||||
Layout::Default(layout) => *layout,
|
||||
let layout = match value.layout {
|
||||
Layout::Default(layout) => layout,
|
||||
Layout::Custom(_) => DefaultLayout::BSP,
|
||||
};
|
||||
|
||||
let name = value
|
||||
.name()
|
||||
.name
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
|
||||
|
||||
Self {
|
||||
layout,
|
||||
name,
|
||||
tile: *value.tile(),
|
||||
workspace_padding: value.workspace_padding().unwrap_or(20),
|
||||
container_padding: value.container_padding().unwrap_or(20),
|
||||
tile: value.tile,
|
||||
workspace_padding: value.workspace_padding.unwrap_or(20),
|
||||
container_padding: value.container_padding.unwrap_or(20),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,7 +247,7 @@ impl eframe::App for KomorebiGui {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ctx.set_pixels_per_point(2.0);
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.set_width(ctx.screen_rect().width());
|
||||
ui.set_width(ctx.content_rect().width());
|
||||
ui.collapsing("Debugging", |ui| {
|
||||
ui.collapsing("Window Rules", |ui| {
|
||||
let window = Window::from(self.debug_hwnd);
|
||||
@@ -437,7 +437,7 @@ impl eframe::App for KomorebiGui {
|
||||
BorderStyle::Square,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
.add(egui::Button::selectable(
|
||||
self.border_config.border_style == option,
|
||||
option.to_string(),
|
||||
))
|
||||
@@ -494,7 +494,7 @@ impl eframe::App for KomorebiGui {
|
||||
StackbarMode::Always,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
.add(egui::Button::selectable(
|
||||
self.stackbar_config.mode == option,
|
||||
option.to_string(),
|
||||
))
|
||||
@@ -513,7 +513,7 @@ impl eframe::App for KomorebiGui {
|
||||
ui.collapsing("Label", |ui| {
|
||||
for option in [StackbarLabel::Process, StackbarLabel::Title] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
.add(egui::Button::selectable(
|
||||
self.stackbar_config.label == option,
|
||||
option.to_string(),
|
||||
))
|
||||
@@ -772,7 +772,7 @@ impl eframe::App for KomorebiGui {
|
||||
DefaultLayout::Grid,
|
||||
] {
|
||||
if ui
|
||||
.add(egui::SelectableLabel::new(
|
||||
.add(egui::Button::selectable(
|
||||
workspace.layout == option,
|
||||
option.to_string(),
|
||||
))
|
||||
|
||||
11
komorebi-shortcuts/Cargo.toml
Normal file
11
komorebi-shortcuts/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "komorebi-shortcuts"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.10" }
|
||||
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.10" }
|
||||
|
||||
eframe = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
98
komorebi-shortcuts/src/main.rs
Normal file
98
komorebi-shortcuts/src/main.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use std::path::PathBuf;
|
||||
use whkd_core::Whkdrc;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Quicklook {
|
||||
whkdrc: Option<Whkdrc>,
|
||||
filter: String,
|
||||
}
|
||||
|
||||
impl Quicklook {
|
||||
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
|
||||
// Restore app state using cc.storage (requires the "persistence" feature).
|
||||
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
|
||||
// for e.g. egui::PaintCallback.
|
||||
let mut home = std::env::var("WHKD_CONFIG_HOME").map_or_else(
|
||||
|_| {
|
||||
dirs::home_dir()
|
||||
.expect("no home directory found")
|
||||
.join(".config")
|
||||
},
|
||||
|home_path| {
|
||||
let home = PathBuf::from(&home_path);
|
||||
|
||||
if home.as_path().is_dir() {
|
||||
home
|
||||
} else {
|
||||
panic!(
|
||||
"$Env:WHKD_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
home.push("whkdrc");
|
||||
|
||||
Self {
|
||||
whkdrc: whkd_parser::load(&home).ok(),
|
||||
filter: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Quicklook {
|
||||
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
|
||||
eframe::egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.set_max_width(ui.available_width());
|
||||
ui.set_max_height(ui.available_height());
|
||||
eframe::egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
eframe::egui::Grid::new("grid")
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.spacing([40.0, 4.0])
|
||||
.min_col_width(ui.available_width() / 2.0 - 20.0)
|
||||
.show(ui, |ui| {
|
||||
if let Some(whkdrc) = &self.whkdrc {
|
||||
ui.label("Filter");
|
||||
ui.add(
|
||||
eframe::egui::text_edit::TextEdit::singleline(&mut self.filter)
|
||||
.hint_text("Filter by command...")
|
||||
.background_color(ctx.style().visuals.faint_bg_color),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
for binding in &whkdrc.bindings {
|
||||
let keys = binding.keys.join(" + ");
|
||||
if self.filter.is_empty() || binding.command.contains(&self.filter)
|
||||
{
|
||||
ui.label(keys);
|
||||
ui.label(&binding.command);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let viewport_builder = ViewportBuilder::default()
|
||||
.with_resizable(true)
|
||||
.with_decorations(false);
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: viewport_builder,
|
||||
centered: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"komorebi-shortcuts",
|
||||
native_options,
|
||||
Box::new(|cc| Ok(Box::new(Quicklook::new(cc)))),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.36"
|
||||
edition = "2021"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "96f26c88d83781f234d42222293ec73d23a39ad8" }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "bdaff30959512c4f7ee7304117076a48633d777f", default-features = false, features = ["egui31"] }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "b9e26b31f7a0e7ed239b14e5317e95d1bdc544bd" }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a", default-features = false, features = [
|
||||
"egui33",
|
||||
] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use hex_color::HexColor;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::Schema;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::InstanceType;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::Schema;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::SchemaObject;
|
||||
use schemars::SchemaGenerator;
|
||||
|
||||
use crate::Color32;
|
||||
use serde::Deserialize;
|
||||
@@ -15,6 +11,7 @@ use serde::Serialize;
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Colour representation
|
||||
pub enum Colour {
|
||||
/// Colour represented as RGB
|
||||
Rgb(Rgb),
|
||||
@@ -56,22 +53,22 @@ impl From<Colour> for Color32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Colour represented as a Hex string
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Hex(pub HexColor);
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for Hex {
|
||||
fn schema_name() -> String {
|
||||
String::from("Hex")
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
std::borrow::Cow::Borrowed("Hex")
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
format: Some("color-hex".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
schemars::json_schema!({
|
||||
"type": "string",
|
||||
"format": "color-hex",
|
||||
"description": "Colour represented as a Hex string"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +83,7 @@ impl From<Colour> for u32 {
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Colour represented as RGB
|
||||
pub struct Rgb {
|
||||
/// Red
|
||||
pub r: u32,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::Base16ColourPalette;
|
||||
use crate::colour::Colour;
|
||||
use crate::colour::Hex;
|
||||
use crate::Base16ColourPalette;
|
||||
use hex_color::HexColor;
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Display;
|
||||
@@ -12,9 +12,12 @@ use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Theme variant
|
||||
pub enum ThemeVariant {
|
||||
#[default]
|
||||
/// Dark variant
|
||||
Dark,
|
||||
/// Light variant
|
||||
Light,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
pub mod colour;
|
||||
mod generator;
|
||||
|
||||
pub use generator::generate_base16_palette;
|
||||
pub use generator::ThemeVariant;
|
||||
pub use generator::generate_base16_palette;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -16,30 +16,31 @@ use strum::IntoEnumIterator;
|
||||
use crate::colour::Colour;
|
||||
pub use base16_egui_themes::Base16;
|
||||
pub use catppuccin_egui;
|
||||
use eframe::egui::style::Selection;
|
||||
use eframe::egui::style::WidgetVisuals;
|
||||
use eframe::egui::style::Widgets;
|
||||
pub use eframe::egui::Color32;
|
||||
use eframe::egui::Shadow;
|
||||
use eframe::egui::Stroke;
|
||||
use eframe::egui::Style;
|
||||
use eframe::egui::Visuals;
|
||||
use eframe::egui::style::Selection;
|
||||
use eframe::egui::style::WidgetVisuals;
|
||||
use eframe::egui::style::Widgets;
|
||||
use serde_variant::to_variant_name;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
/// Theme
|
||||
pub enum Theme {
|
||||
/// A theme from catppuccin-egui
|
||||
/// Theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
name: Catppuccin,
|
||||
accent: Option<CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
/// Theme from base16-egui-themes
|
||||
Base16 {
|
||||
name: Base16,
|
||||
accent: Option<Base16Value>,
|
||||
},
|
||||
/// A custom base16 palette
|
||||
/// Custom base16 palette
|
||||
Custom {
|
||||
palette: Box<Base16ColourPalette>,
|
||||
accent: Option<Base16Value>,
|
||||
@@ -47,22 +48,39 @@ pub enum Theme {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
/// Base16 colour palette: https://github.com/chriskempson/base16
|
||||
pub struct Base16ColourPalette {
|
||||
/// Base00
|
||||
pub base_00: Colour,
|
||||
/// Base01
|
||||
pub base_01: Colour,
|
||||
/// Base02
|
||||
pub base_02: Colour,
|
||||
/// Base03
|
||||
pub base_03: Colour,
|
||||
/// Base04
|
||||
pub base_04: Colour,
|
||||
/// Base05
|
||||
pub base_05: Colour,
|
||||
/// Base06
|
||||
pub base_06: Colour,
|
||||
/// Base07
|
||||
pub base_07: Colour,
|
||||
/// Base08
|
||||
pub base_08: Colour,
|
||||
/// Base09
|
||||
pub base_09: Colour,
|
||||
/// Base0A
|
||||
pub base_0a: Colour,
|
||||
/// Base0B
|
||||
pub base_0b: Colour,
|
||||
/// Base0C
|
||||
pub base_0c: Colour,
|
||||
/// Base0D
|
||||
pub base_0d: Colour,
|
||||
/// Base0E
|
||||
pub base_0e: Colour,
|
||||
/// Base0F
|
||||
pub base_0f: Colour,
|
||||
}
|
||||
|
||||
@@ -199,28 +217,48 @@ impl Theme {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
/// Base16 value
|
||||
pub enum Base16Value {
|
||||
/// Base00
|
||||
Base00,
|
||||
/// Base01
|
||||
Base01,
|
||||
/// Base02
|
||||
Base02,
|
||||
/// Base03
|
||||
Base03,
|
||||
/// Base04
|
||||
Base04,
|
||||
/// Base05
|
||||
Base05,
|
||||
/// Base06
|
||||
#[default]
|
||||
Base06,
|
||||
/// Base07
|
||||
Base07,
|
||||
/// Base08
|
||||
Base08,
|
||||
/// Base09
|
||||
Base09,
|
||||
/// Base0A
|
||||
Base0A,
|
||||
/// Base0B
|
||||
Base0B,
|
||||
/// Base0C
|
||||
Base0C,
|
||||
/// Base0D
|
||||
Base0D,
|
||||
/// Base0E
|
||||
Base0E,
|
||||
/// Base0F
|
||||
Base0F,
|
||||
}
|
||||
|
||||
/// Wrapper around a Base16 colour palette
|
||||
pub enum Base16Wrapper {
|
||||
/// Predefined Base16 colour palette
|
||||
Base16(Base16),
|
||||
/// Custom Base16 colour palette
|
||||
Custom(Box<Base16ColourPalette>),
|
||||
}
|
||||
|
||||
@@ -268,10 +306,15 @@ impl Base16Value {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
/// Catppuccin palette
|
||||
pub enum Catppuccin {
|
||||
/// Frappe (https://catppuccin.com/palette#flavor-frappe)
|
||||
Frappe,
|
||||
/// Latte (https://catppuccin.com/palette#flavor-latte)
|
||||
Latte,
|
||||
/// Macchiato (https://catppuccin.com/palette#flavor-macchiato)
|
||||
Macchiato,
|
||||
/// Mocha (https://catppuccin.com/palette#flavor-mocha)
|
||||
Mocha,
|
||||
}
|
||||
|
||||
@@ -293,33 +336,60 @@ impl From<Catppuccin> for catppuccin_egui::Theme {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
/// Catppuccin Value
|
||||
pub enum CatppuccinValue {
|
||||
/// Rosewater
|
||||
Rosewater,
|
||||
/// Flamingo
|
||||
Flamingo,
|
||||
/// Pink
|
||||
Pink,
|
||||
/// Mauve
|
||||
Mauve,
|
||||
/// Red
|
||||
Red,
|
||||
/// Maroon
|
||||
Maroon,
|
||||
/// Peach
|
||||
Peach,
|
||||
/// Yellow
|
||||
Yellow,
|
||||
/// Green
|
||||
Green,
|
||||
/// Teal
|
||||
Teal,
|
||||
/// Sky
|
||||
Sky,
|
||||
/// Sapphire
|
||||
Sapphire,
|
||||
/// Blue
|
||||
Blue,
|
||||
/// Lavender
|
||||
Lavender,
|
||||
#[default]
|
||||
/// Text
|
||||
Text,
|
||||
/// Subtext1
|
||||
Subtext1,
|
||||
/// Subtext0
|
||||
Subtext0,
|
||||
/// Overlay2
|
||||
Overlay2,
|
||||
/// Overlay1
|
||||
Overlay1,
|
||||
/// Overlay0
|
||||
Overlay0,
|
||||
/// Surface2
|
||||
Surface2,
|
||||
/// Surface1
|
||||
Surface1,
|
||||
/// Surface0
|
||||
Surface0,
|
||||
/// Base
|
||||
Base,
|
||||
/// Mantle
|
||||
Mantle,
|
||||
/// Crust
|
||||
Crust,
|
||||
}
|
||||
|
||||
@@ -359,3 +429,275 @@ impl CatppuccinValue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Theme from catppuccin-egui
|
||||
pub struct KomorebiThemeCatppuccin {
|
||||
/// Name of the Catppuccin theme (previews: https://github.com/catppuccin/catppuccin)
|
||||
pub name: Catppuccin,
|
||||
/// Single window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||
pub single_border: Option<CatppuccinValue>,
|
||||
/// Stack window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Green)))]
|
||||
pub stack_border: Option<CatppuccinValue>,
|
||||
/// Monocle window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Pink)))]
|
||||
pub monocle_border: Option<CatppuccinValue>,
|
||||
/// Floating window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Yellow)))]
|
||||
pub floating_border: Option<CatppuccinValue>,
|
||||
/// Unfocused window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Base)))]
|
||||
pub unfocused_border: Option<CatppuccinValue>,
|
||||
/// Unfocused locked window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Red)))]
|
||||
pub unfocused_locked_border: Option<CatppuccinValue>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar focused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Green)))]
|
||||
pub stackbar_focused_text: Option<CatppuccinValue>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar unfocused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Text)))]
|
||||
pub stackbar_unfocused_text: Option<CatppuccinValue>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar background colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Base)))]
|
||||
pub stackbar_background: Option<CatppuccinValue>,
|
||||
/// Bar accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||
pub bar_accent: Option<CatppuccinValue>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Theme from base16-egui-themes
|
||||
pub struct KomorebiThemeBase16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
pub name: Base16,
|
||||
/// Single window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||
pub single_border: Option<Base16Value>,
|
||||
/// Stack window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||
pub stack_border: Option<Base16Value>,
|
||||
/// Monocle window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0F)))]
|
||||
pub monocle_border: Option<Base16Value>,
|
||||
/// Floating window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base09)))]
|
||||
pub floating_border: Option<Base16Value>,
|
||||
/// Unfocused window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||
pub unfocused_border: Option<Base16Value>,
|
||||
/// Unfocused locked window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base08)))]
|
||||
pub unfocused_locked_border: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar focused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||
pub stackbar_focused_text: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar unfocused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base05)))]
|
||||
pub stackbar_unfocused_text: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar background colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||
pub stackbar_background: Option<Base16Value>,
|
||||
/// Bar accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||
pub bar_accent: Option<Base16Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Custom Base16 theme
|
||||
pub struct KomorebiThemeCustom {
|
||||
/// Colours of the custom Base16 theme palette
|
||||
pub colours: Box<Base16ColourPalette>,
|
||||
/// Single window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||
pub single_border: Option<Base16Value>,
|
||||
/// Stack window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||
pub stack_border: Option<Base16Value>,
|
||||
/// Monocle window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0F)))]
|
||||
pub monocle_border: Option<Base16Value>,
|
||||
/// Floating window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base09)))]
|
||||
pub floating_border: Option<Base16Value>,
|
||||
/// Unfocused window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||
pub unfocused_border: Option<Base16Value>,
|
||||
/// Unfocused locked window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base08)))]
|
||||
pub unfocused_locked_border: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar focused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||
pub stackbar_focused_text: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar unfocused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base05)))]
|
||||
pub stackbar_unfocused_text: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar background colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||
pub stackbar_background: Option<Base16Value>,
|
||||
/// Bar accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||
pub bar_accent: Option<Base16Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
/// Komorebi theme
|
||||
pub enum KomorebiTheme {
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Catppuccin"))]
|
||||
/// Theme from catppuccin-egui
|
||||
Catppuccin(KomorebiThemeCatppuccin),
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Base16"))]
|
||||
/// Theme from base16-egui-themes
|
||||
Base16(KomorebiThemeBase16),
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
/// Custom Base16 theme
|
||||
Custom(KomorebiThemeCustom),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Theme from catppuccin-egui
|
||||
pub struct KomobarThemeCatppuccin {
|
||||
/// Name of the Catppuccin theme (previews: https://github.com/catppuccin/catppuccin)
|
||||
pub name: Catppuccin,
|
||||
/// Accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||
pub accent: Option<CatppuccinValue>,
|
||||
/// Auto select fill colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_fill: Option<CatppuccinValue>,
|
||||
/// Auto select text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_text: Option<CatppuccinValue>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Theme from base16-egui-themes
|
||||
pub struct KomobarThemeBase16 {
|
||||
/// Name of the Base16 theme (previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
pub name: Base16,
|
||||
/// Accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||
pub accent: Option<Base16Value>,
|
||||
/// Auto select fill colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_fill: Option<Base16Value>,
|
||||
/// Auto select text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_text: Option<Base16Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Theme from base16-egui-themes
|
||||
pub struct KomobarThemeCustom {
|
||||
/// Colours of the custom Base16 theme palette
|
||||
pub colours: Box<Base16ColourPalette>,
|
||||
/// Accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||
pub accent: Option<Base16Value>,
|
||||
/// Auto select fill colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_fill: Option<Base16Value>,
|
||||
/// Auto select text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_text: Option<Base16Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
/// Komorebi bar theme
|
||||
pub enum KomobarTheme {
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Catppuccin"))]
|
||||
/// Theme from catppuccin-egui
|
||||
Catppuccin(KomobarThemeCatppuccin),
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Base16"))]
|
||||
/// Theme from base16-egui-themes
|
||||
Base16(KomobarThemeBase16),
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
/// Custom Base16 theme
|
||||
Custom(KomobarThemeCustom),
|
||||
}
|
||||
|
||||
impl From<KomorebiTheme> for KomobarTheme {
|
||||
fn from(value: KomorebiTheme) -> Self {
|
||||
match value {
|
||||
KomorebiTheme::Catppuccin(KomorebiThemeCatppuccin {
|
||||
name, bar_accent, ..
|
||||
}) => Self::Catppuccin(KomobarThemeCatppuccin {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
}),
|
||||
KomorebiTheme::Base16(KomorebiThemeBase16 {
|
||||
name, bar_accent, ..
|
||||
}) => Self::Base16(KomobarThemeBase16 {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
}),
|
||||
KomorebiTheme::Custom(KomorebiThemeCustom {
|
||||
colours,
|
||||
bar_accent,
|
||||
..
|
||||
}) => Self::Custom(KomobarThemeCustom {
|
||||
colours,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.36"
|
||||
version = "0.1.40"
|
||||
description = "A tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
|
||||
base64 = "0.22"
|
||||
bitflags = { version = "2", features = ["serde"] }
|
||||
clap = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
crossbeam-utils = { workspace = true }
|
||||
ctrlc = { version = "3", features = ["termination"] }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
getset = "0.1"
|
||||
ed25519-dalek = "2"
|
||||
hotwatch = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
miow = "0.6"
|
||||
nanoid = "0.4"
|
||||
net2 = "0.2"
|
||||
os_info = "3.10"
|
||||
parking_lot = "0.12"
|
||||
parking_lot = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
powershell_script = "1.0"
|
||||
regex = "1"
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
serde_yaml = { workspace = true }
|
||||
shadow-rs = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
@@ -49,7 +51,7 @@ windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.55"
|
||||
serde_with = { version = "3.12", features = ["schemars_0_8"] }
|
||||
serde_with = { version = "3.12", features = ["schemars_1"] }
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use super::prefix::AnimationPrefix;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -6,10 +6,10 @@ use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::RenderDispatcher;
|
||||
use super::ANIMATION_DURATION_GLOBAL;
|
||||
use super::ANIMATION_FPS;
|
||||
use super::ANIMATION_MANAGER;
|
||||
use super::RenderDispatcher;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
@@ -55,9 +55,9 @@ impl AnimationEngine {
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn animate(
|
||||
render_dispatcher: (impl RenderDispatcher + Send + 'static),
|
||||
render_dispatcher: impl RenderDispatcher + Send + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<()> {
|
||||
) -> eyre::Result<()> {
|
||||
std::thread::spawn(move || {
|
||||
let animation_key = render_dispatcher.get_animation_key();
|
||||
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::core::Rect;
|
||||
use crate::AnimationStyle;
|
||||
use crate::core::Rect;
|
||||
|
||||
use super::style::apply_ease_func;
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ use crate::core::animation::AnimationStyle;
|
||||
use lazy_static::lazy_static;
|
||||
use prefix::AnimationPrefix;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
@@ -25,8 +25,33 @@ use serde::Serialize;
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Animation configuration
|
||||
///
|
||||
/// This can be either global:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "enabled": true,
|
||||
/// "style": "EaseInSine",
|
||||
/// "fps": 60,
|
||||
/// "duration": 250
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Or scoped by an animation kind prefix:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "movement": {
|
||||
/// "enabled": true,
|
||||
/// "style": "EaseInSine",
|
||||
/// "fps": 60,
|
||||
/// "duration": 250
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub enum PerAnimationPrefixConfig<T> {
|
||||
/// Animation configuration prefixed for a specific animation kind
|
||||
Prefix(HashMap<AnimationPrefix, T>),
|
||||
/// Animation configuration for all animation kinds
|
||||
Global(T),
|
||||
}
|
||||
|
||||
|
||||
@@ -17,5 +17,5 @@ pub enum AnimationPrefix {
|
||||
}
|
||||
|
||||
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
|
||||
format!("{}:{}", prefix, key)
|
||||
format!("{prefix}:{key}")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
|
||||
pub trait RenderDispatcher {
|
||||
fn get_animation_key(&self) -> String;
|
||||
fn pre_render(&self) -> Result<()>;
|
||||
fn render(&self, delta: f64) -> Result<()>;
|
||||
fn post_render(&self) -> Result<()>;
|
||||
fn pre_render(&self) -> eyre::Result<()>;
|
||||
fn render(&self, delta: f64) -> eyre::Result<()>;
|
||||
fn post_render(&self) -> eyre::Result<()>;
|
||||
}
|
||||
|
||||
@@ -355,6 +355,61 @@ impl Ease for EaseInOutBounce {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CubicBezier {
|
||||
pub x1: f64,
|
||||
pub y1: f64,
|
||||
pub x2: f64,
|
||||
pub y2: f64,
|
||||
}
|
||||
|
||||
impl CubicBezier {
|
||||
fn x(&self, s: f64) -> f64 {
|
||||
3.0 * self.x1 * s * (1.0 - s).powi(2) + 3.0 * self.x2 * s.powi(2) * (1.0 - s) + s.powi(3)
|
||||
}
|
||||
|
||||
fn y(&self, s: f64) -> f64 {
|
||||
3.0 * self.y1 * s * (1.0 - s).powi(2) + 3.0 * self.y2 * s.powi(2) * (1.0 - s) + s.powi(3)
|
||||
}
|
||||
|
||||
fn dx_ds(&self, s: f64) -> f64 {
|
||||
3.0 * self.x1 * (1.0 - s) * (1.0 - 3.0 * s)
|
||||
+ 3.0 * self.x2 * (2.0 * s - 3.0 * s.powi(2))
|
||||
+ 3.0 * s.powi(2)
|
||||
}
|
||||
|
||||
fn find_s(&self, t: f64) -> f64 {
|
||||
if t <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if t >= 1.0 {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
let mut s = t;
|
||||
|
||||
for _ in 0..8 {
|
||||
let x_val = self.x(s);
|
||||
let dx_val = self.dx_ds(s);
|
||||
if dx_val.abs() < 1e-6 {
|
||||
break;
|
||||
}
|
||||
let delta = (x_val - t) / dx_val;
|
||||
s = (s - delta).clamp(0.0, 1.0);
|
||||
if delta.abs() < 1e-6 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
fn evaluate(&self, t: f64) -> f64 {
|
||||
let s = self.find_s(t.clamp(0.0, 1.0));
|
||||
self.y(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
match style {
|
||||
AnimationStyle::Linear => Linear::evaluate(t),
|
||||
@@ -365,6 +420,7 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
|
||||
AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t),
|
||||
AnimationStyle::EaseOutCubic => EaseOutCubic::evaluate(t),
|
||||
AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
|
||||
AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t),
|
||||
AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t),
|
||||
@@ -387,5 +443,6 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t),
|
||||
AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
|
||||
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
|
||||
AnimationStyle::CubicBezier(x1, y1, x2, y2) => CubicBezier { x1, y1, x2, y2 }.evaluate(t),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::border_manager::RenderTarget;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::WINDOWS_11;
|
||||
use crate::WindowsApi;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::RenderTarget;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::border_manager::WindowKind;
|
||||
use crate::border_manager::window_kind_colour;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::Rect;
|
||||
use crate::windows_api;
|
||||
use crate::WindowsApi;
|
||||
use crate::WINDOWS_11;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::LazyLock;
|
||||
use windows::Win32::Foundation::FALSE;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::LRESULT;
|
||||
use windows::Win32::Foundation::TRUE;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
|
||||
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
|
||||
@@ -36,31 +33,34 @@ use windows::Win32::Graphics::Direct2D::D2D1_PRESENT_OPTIONS_IMMEDIATELY;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
|
||||
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
|
||||
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
|
||||
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
|
||||
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
|
||||
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
|
||||
use windows::Win32::Graphics::Gdi::CreateRectRgn;
|
||||
use windows::Win32::Graphics::Gdi::InvalidateRect;
|
||||
use windows::Win32::Graphics::Gdi::ValidateRect;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
|
||||
@@ -102,10 +102,10 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
|
||||
let hwnd = hwnd.0 as isize;
|
||||
|
||||
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
|
||||
if class.starts_with("komoborder") {
|
||||
hwnds.push(hwnd);
|
||||
}
|
||||
if let Ok(class) = WindowsApi::real_window_class_w(hwnd)
|
||||
&& class.starts_with("komoborder")
|
||||
{
|
||||
hwnds.push(hwnd);
|
||||
}
|
||||
|
||||
true.into()
|
||||
@@ -313,6 +313,11 @@ impl Border {
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
// clear user data **BEFORE** closing window
|
||||
// pending messages will see a null pointer and exit early
|
||||
unsafe {
|
||||
SetWindowLongPtrW(self.hwnd(), GWLP_USERDATA, 0);
|
||||
}
|
||||
WindowsApi::close_window(self.hwnd)
|
||||
}
|
||||
|
||||
@@ -392,63 +397,63 @@ impl Border {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if !rect.is_same_size_as(&old_rect) {
|
||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
if (!rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect))
|
||||
&& let Some(render_target) = (*border_pointer).render_target.as_ref()
|
||||
{
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
|
||||
left: (border_width / 2 - border_offset) as f32,
|
||||
top: (border_width / 2 - border_offset) as f32,
|
||||
right: (rect.right - border_width / 2 + border_offset) as f32,
|
||||
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
let _ = render_target.Resize(&D2D_SIZE_U {
|
||||
width: rect.right as u32,
|
||||
height: rect.bottom as u32,
|
||||
});
|
||||
|
||||
let window_kind = (*border_pointer).window_kind;
|
||||
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
|
||||
render_target.BeginDraw();
|
||||
render_target.Clear(None);
|
||||
|
||||
// Calculate border radius based on style
|
||||
let style = match (*border_pointer).style {
|
||||
BorderStyle::System => {
|
||||
if *WINDOWS_11 {
|
||||
BorderStyle::Rounded
|
||||
} else {
|
||||
BorderStyle::Square
|
||||
}
|
||||
}
|
||||
BorderStyle::Rounded => BorderStyle::Rounded,
|
||||
BorderStyle::Square => BorderStyle::Square,
|
||||
};
|
||||
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
match style {
|
||||
BorderStyle::Rounded => {
|
||||
render_target.DrawRoundedRectangle(
|
||||
&(*border_pointer).rounded_rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
BorderStyle::Square => {
|
||||
render_target.DrawRectangle(
|
||||
&(*border_pointer).rounded_rect.rect,
|
||||
brush,
|
||||
border_width as f32,
|
||||
None,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = render_target.EndDraw(None, None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod border;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::WindowKind;
|
||||
use crate::ring::Ring;
|
||||
use crate::windows_api;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
use border::border_hwnds;
|
||||
pub use border::Border;
|
||||
use border::border_hwnds;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
@@ -21,15 +22,15 @@ use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use strum::Display;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
|
||||
@@ -169,13 +170,15 @@ fn window_kind_colour(focus_kind: WindowKind) -> u32 {
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -208,10 +211,12 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
.iter()
|
||||
.map(|w| w.hwnd)
|
||||
.collect::<Vec<_>>();
|
||||
let workspace_layer = *state.monitors.elements()[focused_monitor_idx].workspaces()
|
||||
let workspace_layer = state.monitors.elements()[focused_monitor_idx].workspaces()
|
||||
[focused_workspace_idx]
|
||||
.layer();
|
||||
.layer;
|
||||
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
let forced_update = matches!(notification, Notification::ForceUpdate);
|
||||
|
||||
drop(state);
|
||||
|
||||
@@ -221,7 +226,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
if let Some(monocle) = &ws.monocle_container {
|
||||
let window_kind = if monitor_idx != focused_monitor_idx {
|
||||
WindowKind::Unfocused
|
||||
} else {
|
||||
@@ -234,6 +239,17 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
.unwrap_or_default()
|
||||
.set_accent(window_kind_colour(window_kind))?;
|
||||
|
||||
if ws.layer == WorkspaceLayer::Floating {
|
||||
for window in ws.floating_windows() {
|
||||
let mut window_kind = WindowKind::Unfocused;
|
||||
|
||||
if foreground_window == window.hwnd {
|
||||
window_kind = WindowKind::Floating;
|
||||
}
|
||||
|
||||
window.set_accent(window_kind_colour(window_kind))?;
|
||||
}
|
||||
}
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
@@ -241,7 +257,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let window_kind = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
if c.locked {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
@@ -325,15 +341,11 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
if !should_process_notification {
|
||||
if let Some(Notification::Update(ref previous)) = previous_notification
|
||||
{
|
||||
if previous.unwrap_or_default()
|
||||
!= notification_hwnd.unwrap_or_default()
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
}
|
||||
if !should_process_notification
|
||||
&& let Some(Notification::Update(ref previous)) = previous_notification
|
||||
&& previous.unwrap_or_default() != notification_hwnd.unwrap_or_default()
|
||||
{
|
||||
should_process_notification = true;
|
||||
}
|
||||
|
||||
should_process_notification
|
||||
@@ -342,7 +354,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
};
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
tracing::debug!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
@@ -369,7 +381,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
// Only operate on the focused workspace of each monitor
|
||||
if let Some(ws) = m.focused_workspace() {
|
||||
// Workspaces with tiling disabled don't have borders
|
||||
if !ws.tile() {
|
||||
if !ws.tile {
|
||||
// Remove all borders on this monitor
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
@@ -382,16 +394,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
}
|
||||
|
||||
// Handle the monocle container separately
|
||||
if let Some(monocle) = ws.monocle_container() {
|
||||
if let Some(monocle) = &ws.monocle_container {
|
||||
let mut new_border = false;
|
||||
let focused_window_hwnd =
|
||||
monocle.focused_window().map(|w| w.hwnd).unwrap_or_default();
|
||||
let id = monocle.id().clone();
|
||||
let id = monocle.id.clone();
|
||||
let border = match borders.entry(id.clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(
|
||||
monocle.id(),
|
||||
&monocle.id,
|
||||
focused_window_hwnd,
|
||||
monitor_idx,
|
||||
) {
|
||||
@@ -448,22 +460,48 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
windows_borders.insert(focused_window_hwnd, id);
|
||||
|
||||
let border_hwnd = border.hwnd;
|
||||
// Remove all borders on this monitor except monocle
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| border_hwnd != b.hwnd,
|
||||
)?;
|
||||
|
||||
if ws.layer == WorkspaceLayer::Floating {
|
||||
handle_floating_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
ws,
|
||||
monitor_idx,
|
||||
foreground_window,
|
||||
layer_changed,
|
||||
forced_update,
|
||||
)?;
|
||||
|
||||
// Remove all borders on this monitor except monocle and floating borders
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| {
|
||||
border_hwnd != b.hwnd
|
||||
&& !ws
|
||||
.floating_windows()
|
||||
.iter()
|
||||
.any(|w| w.hwnd == b.tracking_hwnd)
|
||||
},
|
||||
)?;
|
||||
} else {
|
||||
// Remove all borders on this monitor except monocle
|
||||
remove_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
monitor_idx,
|
||||
|_, b| border_hwnd != b.hwnd,
|
||||
)?;
|
||||
}
|
||||
continue 'monitors;
|
||||
}
|
||||
|
||||
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
|
||||
let foreground_monitor_id =
|
||||
WindowsApi::monitor_from_window(foreground_hwnd);
|
||||
let is_maximized = foreground_monitor_id == m.id()
|
||||
&& WindowsApi::is_zoomed(foreground_hwnd);
|
||||
let is_maximized =
|
||||
foreground_monitor_id == m.id && WindowsApi::is_zoomed(foreground_hwnd);
|
||||
|
||||
if is_maximized {
|
||||
// Remove all borders on this monitor
|
||||
@@ -481,7 +519,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let mut container_and_floating_window_ids = ws
|
||||
.containers()
|
||||
.iter()
|
||||
.map(|c| c.id().clone())
|
||||
.map(|c| c.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for w in ws.floating_windows() {
|
||||
@@ -499,7 +537,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
'containers: for (idx, c) in ws.containers().iter().enumerate() {
|
||||
let focused_window_hwnd =
|
||||
c.focused_window().map(|w| w.hwnd).unwrap_or_default();
|
||||
let id = c.id().clone();
|
||||
let id = c.id.clone();
|
||||
|
||||
// Get the border entry for this container from the map or create one
|
||||
let mut new_border = false;
|
||||
@@ -507,7 +545,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) =
|
||||
Border::create(c.id(), focused_window_hwnd, monitor_idx)
|
||||
Border::create(&c.id, focused_window_hwnd, monitor_idx)
|
||||
{
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
@@ -523,7 +561,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
|| focused_window_hwnd != foreground_window
|
||||
{
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
if c.locked {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
@@ -563,15 +601,12 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let rect = match WindowsApi::window_rect(focused_window_hwnd) {
|
||||
Ok(rect) => rect,
|
||||
Err(_) => {
|
||||
remove_border(c.id(), &mut borders, &mut windows_borders)?;
|
||||
remove_border(&c.id, &mut borders, &mut windows_borders)?;
|
||||
continue 'containers;
|
||||
}
|
||||
};
|
||||
border.window_rect = rect;
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
let forced_update = matches!(notification, Notification::ForceUpdate);
|
||||
|
||||
let should_invalidate = new_border
|
||||
|| (last_focus_state != new_focus_state)
|
||||
|| layer_changed
|
||||
@@ -591,65 +626,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
windows_borders.insert(focused_window_hwnd, id);
|
||||
}
|
||||
|
||||
{
|
||||
for window in ws.floating_windows() {
|
||||
let mut new_border = false;
|
||||
let id = window.hwnd.to_string();
|
||||
let border = match borders.entry(id.clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) = Border::create(
|
||||
&window.hwnd.to_string(),
|
||||
window.hwnd,
|
||||
monitor_idx,
|
||||
) {
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
} else {
|
||||
continue 'monitors;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let last_focus_state = border.window_kind;
|
||||
|
||||
let new_focus_state = if foreground_window == window.hwnd {
|
||||
WindowKind::Floating
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
};
|
||||
|
||||
border.window_kind = new_focus_state;
|
||||
|
||||
// Update the border's monitor idx in case it changed
|
||||
border.monitor_idx = Some(monitor_idx);
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
border.window_rect = rect;
|
||||
|
||||
let layer_changed = previous_layer != workspace_layer;
|
||||
let forced_update =
|
||||
matches!(notification, Notification::ForceUpdate);
|
||||
|
||||
let should_invalidate = new_border
|
||||
|| (last_focus_state != new_focus_state)
|
||||
|| layer_changed
|
||||
|| forced_update;
|
||||
|
||||
if should_invalidate {
|
||||
if forced_update && !new_border {
|
||||
// Update the border brushes if there was a forced update
|
||||
// notification and this is not a new border (new border's
|
||||
// already have their brushes updated on creation)
|
||||
border.update_brushes()?;
|
||||
}
|
||||
border.set_position(&rect, window.hwnd)?;
|
||||
border.invalidate();
|
||||
}
|
||||
|
||||
windows_borders.insert(window.hwnd, id);
|
||||
}
|
||||
}
|
||||
handle_floating_borders(
|
||||
&mut borders,
|
||||
&mut windows_borders,
|
||||
ws,
|
||||
monitor_idx,
|
||||
foreground_window,
|
||||
layer_changed,
|
||||
forced_update,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -665,6 +650,68 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_floating_borders(
|
||||
borders: &mut HashMap<String, Box<Border>>,
|
||||
windows_borders: &mut HashMap<isize, String>,
|
||||
ws: &Workspace,
|
||||
monitor_idx: usize,
|
||||
foreground_window: isize,
|
||||
layer_changed: bool,
|
||||
forced_update: bool,
|
||||
) -> color_eyre::Result<()> {
|
||||
for window in ws.floating_windows() {
|
||||
let mut new_border = false;
|
||||
let id = window.hwnd.to_string();
|
||||
let border = match borders.entry(id.clone()) {
|
||||
Entry::Occupied(entry) => entry.into_mut(),
|
||||
Entry::Vacant(entry) => {
|
||||
if let Ok(border) =
|
||||
Border::create(&window.hwnd.to_string(), window.hwnd, monitor_idx)
|
||||
{
|
||||
new_border = true;
|
||||
entry.insert(border)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let last_focus_state = border.window_kind;
|
||||
|
||||
let new_focus_state = if foreground_window == window.hwnd {
|
||||
WindowKind::Floating
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
};
|
||||
|
||||
border.window_kind = new_focus_state;
|
||||
|
||||
// Update the border's monitor idx in case it changed
|
||||
border.monitor_idx = Some(monitor_idx);
|
||||
|
||||
let rect = WindowsApi::window_rect(window.hwnd)?;
|
||||
border.window_rect = rect;
|
||||
|
||||
let should_invalidate =
|
||||
new_border || (last_focus_state != new_focus_state) || layer_changed || forced_update;
|
||||
|
||||
if should_invalidate {
|
||||
if forced_update && !new_border {
|
||||
// Update the border brushes if there was a forced update
|
||||
// notification and this is not a new border (new border's
|
||||
// already have their brushes updated on creation)
|
||||
border.update_brushes()?;
|
||||
}
|
||||
border.set_position(&rect, window.hwnd)?;
|
||||
border.invalidate();
|
||||
}
|
||||
|
||||
windows_borders.insert(window.hwnd, id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes all borders from monitor with index `monitor_idx` filtered by
|
||||
/// `condition`. This condition is a function that will take a reference to
|
||||
/// the container id and the border and returns a bool, if true that border
|
||||
@@ -720,6 +767,13 @@ fn remove_border(
|
||||
fn destroy_border(border: Box<Border>) -> color_eyre::Result<()> {
|
||||
let raw_pointer = Box::into_raw(border);
|
||||
unsafe {
|
||||
// release d2d resources **BEFORE** destroying window
|
||||
// this drops render_target and brushes while HWND is still valid
|
||||
// prevents EndDraw() from accessing freed HWND resources
|
||||
(*raw_pointer).render_target = None;
|
||||
(*raw_pointer).brushes.clear();
|
||||
|
||||
// Now safe to destroy window
|
||||
(*raw_pointer).destroy()?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -766,10 +820,26 @@ pub fn hide_border(tracking_hwnd: isize) {
|
||||
|
||||
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Z Order (https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos)
|
||||
pub enum ZOrder {
|
||||
/// HWND_TOP
|
||||
///
|
||||
/// Places the window at the top of the Z order.
|
||||
Top,
|
||||
/// HWND_NOTOPMOST
|
||||
///
|
||||
/// Places the window above all non-topmost windows (that is, behind all topmost windows).
|
||||
/// This flag has no effect if the window is already a non-topmost window.
|
||||
NoTopMost,
|
||||
/// HWND_BOTTOM
|
||||
///
|
||||
/// Places the window at the bottom of the Z order. If the hWnd parameter identifies a topmost window,
|
||||
/// the window loses its topmost status and is placed at the bottom of all other windows.
|
||||
Bottom,
|
||||
/// HWND_TOPMOST
|
||||
///
|
||||
/// Places the window above all non-topmost windows.
|
||||
/// The window maintains its topmost position even when it is deactivated.
|
||||
TopMost,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::ops::Deref;
|
||||
use windows::core::IUnknown;
|
||||
use windows::core::IUnknown_Vtbl;
|
||||
use windows::core::GUID;
|
||||
use windows::core::HRESULT;
|
||||
use windows::core::HSTRING;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::core::PWSTR;
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::RECT;
|
||||
use windows::Win32::Foundation::SIZE;
|
||||
use windows::Win32::UI::Shell::Common::IObjectArray;
|
||||
use windows::core::GUID;
|
||||
use windows::core::HRESULT;
|
||||
use windows::core::HSTRING;
|
||||
use windows::core::IUnknown;
|
||||
use windows::core::IUnknown_Vtbl;
|
||||
use windows::core::PCWSTR;
|
||||
use windows::core::PWSTR;
|
||||
use windows_core::BOOL;
|
||||
|
||||
type DesktopID = GUID;
|
||||
@@ -129,7 +129,7 @@ pub unsafe trait IApplicationView: IUnknown {
|
||||
pub unsafe fn get_app_user_model_id(&self, id: *mut PWSTR) -> HRESULT; // Proc17
|
||||
pub unsafe fn set_app_user_model_id(&self, id: PCWSTR) -> HRESULT;
|
||||
pub unsafe fn is_equal_by_app_user_model_id(&self, id: PCWSTR, out_result: *mut INT)
|
||||
-> HRESULT;
|
||||
-> HRESULT;
|
||||
|
||||
/*** IApplicationView methods ***/
|
||||
pub unsafe fn get_view_state(&self, out_state: *mut UINT) -> HRESULT; // Proc20
|
||||
|
||||
@@ -11,11 +11,11 @@ use interfaces::IServiceProvider;
|
||||
use std::ffi::c_void;
|
||||
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||
use windows::Win32::System::Com::COINIT_MULTITHREADED;
|
||||
use windows::Win32::System::Com::CoCreateInstance;
|
||||
use windows::Win32::System::Com::CoInitializeEx;
|
||||
use windows::Win32::System::Com::CoUninitialize;
|
||||
use windows::Win32::System::Com::CLSCTX_ALL;
|
||||
use windows::Win32::System::Com::COINIT_MULTITHREADED;
|
||||
use windows_core::Interface;
|
||||
|
||||
struct ComInit();
|
||||
@@ -64,7 +64,7 @@ fn get_iapplication_view_collection(provider: &IServiceProvider) -> IApplication
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
|
||||
COM_INIT.with(|_| {
|
||||
let provider = get_iservice_provider();
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use getset::Getters;
|
||||
use nanoid::nanoid;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Lockable;
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Container {
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub locked: bool,
|
||||
windows: Ring<Window>,
|
||||
}
|
||||
|
||||
@@ -22,22 +23,45 @@ impl Default for Container {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: nanoid!(),
|
||||
locked: false,
|
||||
windows: Ring::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Lockable for Container {
|
||||
fn locked(&self) -> bool {
|
||||
self.locked
|
||||
}
|
||||
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self {
|
||||
self.locked = locked;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn preselect() -> Self {
|
||||
Self {
|
||||
id: "PRESELECT".to_string(),
|
||||
locked: false,
|
||||
windows: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_preselect(&self) -> bool {
|
||||
self.id == "PRESELECT"
|
||||
}
|
||||
|
||||
pub fn hide(&self, omit: Option<isize>) {
|
||||
for window in self.windows().iter().rev() {
|
||||
let mut should_hide = omit.is_none();
|
||||
|
||||
if !should_hide {
|
||||
if let Some(omit) = omit {
|
||||
if omit != window.hwnd {
|
||||
should_hide = true
|
||||
}
|
||||
}
|
||||
if !should_hide
|
||||
&& let Some(omit) = omit
|
||||
&& omit != window.hwnd
|
||||
{
|
||||
should_hide = true
|
||||
}
|
||||
|
||||
if should_hide {
|
||||
@@ -69,10 +93,10 @@ impl Container {
|
||||
|
||||
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
|
||||
for window in self.windows() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(window.hwnd);
|
||||
}
|
||||
if let Ok(window_exe) = window.exe()
|
||||
&& exe == window_exe
|
||||
{
|
||||
return Option::from(window.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +105,10 @@ impl Container {
|
||||
|
||||
pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
|
||||
for (idx, window) in self.windows().iter().enumerate() {
|
||||
if let Ok(window_exe) = window.exe() {
|
||||
if exe == window_exe {
|
||||
return Option::from(idx);
|
||||
}
|
||||
if let Ok(window_exe) = window.exe()
|
||||
&& exe == window_exe
|
||||
{
|
||||
return Option::from(idx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +168,7 @@ impl Container {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json;
|
||||
|
||||
#[test]
|
||||
fn test_contains_window() {
|
||||
@@ -250,4 +275,40 @@ mod tests {
|
||||
// Should return None since window 4 doesn't exist
|
||||
assert_eq!(container.idx_for_window(4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_with_missing_locked_field_defaults_to_false() {
|
||||
let json = r#"{
|
||||
"id": "test-1",
|
||||
"windows": { "elements": [], "focused": 0 }
|
||||
}"#;
|
||||
let container: Container = serde_json::from_str(json).expect("Should deserialize");
|
||||
|
||||
assert!(!container.locked);
|
||||
assert_eq!(container.id, "test-1");
|
||||
assert!(container.windows().is_empty());
|
||||
|
||||
let json = r#"{
|
||||
"id": "test-2",
|
||||
"windows": { "elements": [ { "hwnd": 5 }, { "hwnd": 9 } ], "focused": 1 }
|
||||
}"#;
|
||||
let container: Container = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(container.id, "test-2");
|
||||
assert!(!container.locked);
|
||||
assert_eq!(container.windows(), &[Window::from(5), Window::from(9)]);
|
||||
assert_eq!(container.focused_window_idx(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_and_deserializes() {
|
||||
let mut container = Container::default();
|
||||
container.set_locked(true);
|
||||
|
||||
let serialized = serde_json::to_string(&container).expect("Should serialize");
|
||||
let deserialized: Container =
|
||||
serde_json::from_str(&serialized).expect("Should deserialize");
|
||||
|
||||
assert!(deserialized.locked);
|
||||
assert_eq!(deserialized.id, container.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,40 +2,153 @@ use clap::ValueEnum;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::ser::SerializeSeq;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Mathematical function which describes the rate at which a value changes
|
||||
pub enum AnimationStyle {
|
||||
/// Linear
|
||||
Linear,
|
||||
/// Ease in sine
|
||||
EaseInSine,
|
||||
/// Ease out sine
|
||||
EaseOutSine,
|
||||
/// Ease in out sine
|
||||
EaseInOutSine,
|
||||
/// Ease in quad
|
||||
EaseInQuad,
|
||||
/// Ease out quad
|
||||
EaseOutQuad,
|
||||
/// Ease in out quad
|
||||
EaseInOutQuad,
|
||||
/// Ease in cubic
|
||||
EaseInCubic,
|
||||
/// Ease out cubic
|
||||
EaseOutCubic,
|
||||
/// Ease in out cubic
|
||||
EaseInOutCubic,
|
||||
/// Ease in quart
|
||||
EaseInQuart,
|
||||
/// Ease out quart
|
||||
EaseOutQuart,
|
||||
/// Ease in out quart
|
||||
EaseInOutQuart,
|
||||
/// Ease in quint
|
||||
EaseInQuint,
|
||||
/// Ease out quint
|
||||
EaseOutQuint,
|
||||
/// Ease in out quint
|
||||
EaseInOutQuint,
|
||||
/// Ease in expo
|
||||
EaseInExpo,
|
||||
/// Ease out expo
|
||||
EaseOutExpo,
|
||||
/// Ease in out expo
|
||||
EaseInOutExpo,
|
||||
/// Ease in circ
|
||||
EaseInCirc,
|
||||
/// Ease out circ
|
||||
EaseOutCirc,
|
||||
/// Ease in out circ
|
||||
EaseInOutCirc,
|
||||
/// Ease in back
|
||||
EaseInBack,
|
||||
/// Ease out back
|
||||
EaseOutBack,
|
||||
/// Ease in out back
|
||||
EaseInOutBack,
|
||||
/// Ease in elastic
|
||||
EaseInElastic,
|
||||
/// Ease out elastic
|
||||
EaseOutElastic,
|
||||
/// Ease in out elastic
|
||||
EaseInOutElastic,
|
||||
/// Ease in bounce
|
||||
EaseInBounce,
|
||||
/// Ease out bounce
|
||||
EaseOutBounce,
|
||||
/// Ease in out bounce
|
||||
EaseInOutBounce,
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "CubicBezier"))]
|
||||
#[value(skip)]
|
||||
/// Custom Cubic Bézier function
|
||||
CubicBezier(f64, f64, f64, f64),
|
||||
}
|
||||
|
||||
// Custom serde implementation
|
||||
impl<'de> Deserialize<'de> for AnimationStyle {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct AnimationStyleVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for AnimationStyleVisitor {
|
||||
type Value = AnimationStyle;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string or an array of four f64 values")
|
||||
}
|
||||
|
||||
// Handle string variants (e.g., "EaseInOutExpo")
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
value.parse().map_err(|_| E::unknown_variant(value, &[]))
|
||||
}
|
||||
|
||||
// Handle CubicBezier array (e.g., [0.32, 0.72, 0.0, 1.0])
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let x1 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
|
||||
let y1 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
|
||||
let x2 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
|
||||
let y2 = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
|
||||
|
||||
// Ensure no extra elements
|
||||
if seq.next_element::<serde::de::IgnoredAny>()?.is_some() {
|
||||
return Err(serde::de::Error::invalid_length(5, &self));
|
||||
}
|
||||
|
||||
Ok(AnimationStyle::CubicBezier(x1, y1, x2, y2))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(AnimationStyleVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AnimationStyle {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
// Serialize CubicBezier as an array
|
||||
AnimationStyle::CubicBezier(x1, y1, x2, y2) => {
|
||||
let mut seq = serializer.serialize_seq(Some(4))?;
|
||||
seq.serialize_element(x1)?;
|
||||
seq.serialize_element(y1)?;
|
||||
seq.serialize_element(x2)?;
|
||||
seq.serialize_element(y2)?;
|
||||
seq.end()
|
||||
}
|
||||
// Serialize all other variants as strings
|
||||
_ => serializer.serialize_str(&self.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@ 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 super::custom_layout::Column;
|
||||
use super::custom_layout::ColumnSplit;
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
|
||||
pub trait Arrangement {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
@@ -21,6 +23,9 @@ pub trait Arrangement {
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
focused_idx: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
latest_layout: &[Rect],
|
||||
) -> Vec<Rect>;
|
||||
}
|
||||
|
||||
@@ -33,9 +38,99 @@ impl Arrangement for DefaultLayout {
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
focused_idx: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
latest_layout: &[Rect],
|
||||
) -> Vec<Rect> {
|
||||
let len = usize::from(len);
|
||||
let mut dimensions = match self {
|
||||
Self::Scrolling => {
|
||||
let column_count = layout_options
|
||||
.and_then(|o| o.scrolling.map(|s| s.columns))
|
||||
.unwrap_or(3);
|
||||
|
||||
let column_width = area.right / column_count.min(len) as i32;
|
||||
let mut layouts = Vec::with_capacity(len);
|
||||
|
||||
let visible_columns = area.right / column_width;
|
||||
let keep_centered = layout_options
|
||||
.and_then(|o| {
|
||||
o.scrolling
|
||||
.map(|s| s.center_focused_column.unwrap_or_default())
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let first_visible: isize = if focused_idx == 0 {
|
||||
// if focused idx is 0, we are at the beginning of the scrolling strip
|
||||
0
|
||||
} else {
|
||||
let previous_first_visible = if latest_layout.is_empty() {
|
||||
0
|
||||
} else {
|
||||
// previous first_visible based on the left position of the first visible window
|
||||
let left_edge = area.left;
|
||||
latest_layout
|
||||
.iter()
|
||||
.position(|rect| rect.left >= left_edge)
|
||||
.unwrap_or(0) as isize
|
||||
};
|
||||
|
||||
let focused_idx = focused_idx as isize;
|
||||
|
||||
// if center_focused_column is enabled, and we have an odd number of visible columns,
|
||||
// center the focused window column
|
||||
if keep_centered && visible_columns % 2 == 1 {
|
||||
let center_offset = visible_columns as isize / 2;
|
||||
(focused_idx - center_offset).max(0).min(
|
||||
(len as isize)
|
||||
.saturating_sub(visible_columns as isize)
|
||||
.max(0),
|
||||
)
|
||||
} else {
|
||||
if focused_idx < previous_first_visible {
|
||||
// focused window is off the left edge, we need to scroll left
|
||||
focused_idx
|
||||
} else if focused_idx >= previous_first_visible + visible_columns as isize {
|
||||
// focused window is off the right edge, we need to scroll right
|
||||
// and make sure it's the last visible window
|
||||
(focused_idx + 1 - visible_columns as isize).max(0)
|
||||
} else {
|
||||
// focused window is already visible, we don't need to scroll
|
||||
previous_first_visible
|
||||
}
|
||||
.min(
|
||||
(len as isize)
|
||||
.saturating_sub(visible_columns as isize)
|
||||
.max(0),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
for i in 0..len {
|
||||
let position = (i as isize) - first_visible;
|
||||
let left = area.left + (position as i32 * column_width);
|
||||
|
||||
layouts.push(Rect {
|
||||
left,
|
||||
top: area.top,
|
||||
right: column_width,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
|
||||
layouts
|
||||
.iter_mut()
|
||||
.zip(adjustment.iter())
|
||||
.for_each(|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
});
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::BSP => recursive_fibonacci(
|
||||
0,
|
||||
len,
|
||||
@@ -60,10 +155,9 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
columns_reverse(&mut layouts);
|
||||
}
|
||||
) && let 2.. = len
|
||||
{
|
||||
columns_reverse(&mut layouts);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -85,10 +179,9 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
rows_reverse(&mut layouts);
|
||||
}
|
||||
) && let 2.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -139,25 +232,23 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
) && let 2.. = len
|
||||
{
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left;
|
||||
}
|
||||
primary.left = rest[0].left + rest[0].right;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left;
|
||||
}
|
||||
primary.left = rest[0].left + rest[0].right;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 3.. = len {
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
) && let 3.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -211,25 +302,23 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
) && let 2.. = len
|
||||
{
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
primary.left = rest[0].left;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left + primary.right;
|
||||
}
|
||||
primary.left = rest[0].left;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.left = primary.left + primary.right;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 3.. = len {
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
) && let 3.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts[1..]);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -280,25 +369,23 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
) && let 2.. = len
|
||||
{
|
||||
let (primary, rest) = layouts.split_at_mut(1);
|
||||
let primary = &mut primary[0];
|
||||
|
||||
for rect in rest.iter_mut() {
|
||||
rect.top = primary.top;
|
||||
}
|
||||
primary.top = rest[0].top + rest[0].bottom;
|
||||
for rect in rest.iter_mut() {
|
||||
rect.top = primary.top;
|
||||
}
|
||||
primary.top = rest[0].top + rest[0].bottom;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 3.. = len {
|
||||
columns_reverse(&mut layouts[1..]);
|
||||
}
|
||||
) && let 3.. = len
|
||||
{
|
||||
columns_reverse(&mut layouts[1..]);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -407,10 +494,9 @@ impl Arrangement for DefaultLayout {
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Vertical | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 4.. = len {
|
||||
rows_reverse(&mut layouts[2..]);
|
||||
}
|
||||
) && let 4.. = len
|
||||
{
|
||||
rows_reverse(&mut layouts[2..]);
|
||||
}
|
||||
|
||||
layouts
|
||||
@@ -428,14 +514,25 @@ impl Arrangement for DefaultLayout {
|
||||
|
||||
let len = len as i32;
|
||||
|
||||
let num_cols = (len as f32).sqrt().ceil() as i32;
|
||||
let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
|
||||
let num_cols = if let Some(rows) = row_constraint {
|
||||
((len as f32) / (rows as f32)).ceil() as i32
|
||||
} else {
|
||||
(len as f32).sqrt().ceil() as i32
|
||||
};
|
||||
|
||||
let mut iter = layouts.iter_mut().enumerate().peekable();
|
||||
|
||||
for col in 0..num_cols {
|
||||
let iter_peek = iter.peek().map(|x| x.0).unwrap_or_default() as i32;
|
||||
let remaining_windows = len - iter_peek;
|
||||
let remaining_columns = num_cols - col;
|
||||
let num_rows_in_this_col = remaining_windows / remaining_columns;
|
||||
|
||||
let num_rows_in_this_col = if let Some(rows) = row_constraint {
|
||||
(remaining_windows / remaining_columns).min(rows as i32)
|
||||
} else {
|
||||
remaining_windows / remaining_columns
|
||||
};
|
||||
|
||||
let win_height = area.bottom / num_rows_in_this_col;
|
||||
let win_width = area.right / num_cols;
|
||||
@@ -487,6 +584,9 @@ impl Arrangement for CustomLayout {
|
||||
container_padding: Option<i32>,
|
||||
_layout_flip: Option<Axis>,
|
||||
_resize_dimensions: &[Option<Rect>],
|
||||
_focused_idx: usize,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
_latest_layout: &[Rect],
|
||||
) -> Vec<Rect> {
|
||||
let mut dimensions = vec![];
|
||||
let container_count = len.get();
|
||||
@@ -541,7 +641,7 @@ impl Arrangement for CustomLayout {
|
||||
};
|
||||
|
||||
match column {
|
||||
Column::Primary(Option::Some(_)) => {
|
||||
Column::Primary(Some(_)) => {
|
||||
let main_column_area = if idx == 0 {
|
||||
Self::main_column_area(area, primary_right, None)
|
||||
} else {
|
||||
@@ -604,9 +704,13 @@ impl Arrangement for CustomLayout {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Axis on which to perform an operation
|
||||
pub enum Axis {
|
||||
/// Horizontal axis
|
||||
Horizontal,
|
||||
/// Vertical axis
|
||||
Vertical,
|
||||
/// Both horizontal and vertical axes
|
||||
HorizontalAndVertical,
|
||||
}
|
||||
|
||||
@@ -673,67 +777,67 @@ fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Optio
|
||||
|
||||
// This needs to be aware of layout flips
|
||||
for (i, opt) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(resize_ref) = opt {
|
||||
if i > 0 {
|
||||
if resize_ref.left != 0 {
|
||||
#[allow(clippy::if_not_else)]
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 != 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
if let Some(resize_ref) = opt
|
||||
&& i > 0
|
||||
{
|
||||
if resize_ref.left != 0 {
|
||||
#[allow(clippy::if_not_else)]
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 != 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 == 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.right += resize_ref.left;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: resize_ref.left,
|
||||
bottom: 0,
|
||||
});
|
||||
}
|
||||
for n in range {
|
||||
let should_adjust = n % 2 == 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.right += resize_ref.left;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: resize_ref.left,
|
||||
bottom: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||
rr.left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if resize_ref.top != 0 {
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 == 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
if let Some(rr) = resize_adjustments[i].as_mut() {
|
||||
rr.left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 != 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.bottom += resize_ref.top;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: resize_ref.top,
|
||||
});
|
||||
}
|
||||
if resize_ref.top != 0 {
|
||||
let range = if i == 1 {
|
||||
0..1
|
||||
} else if i & 1 == 0 {
|
||||
i - 1..i
|
||||
} else {
|
||||
i - 2..i
|
||||
};
|
||||
|
||||
for n in range {
|
||||
let should_adjust = n % 2 != 0;
|
||||
if should_adjust {
|
||||
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
|
||||
adjacent_resize.bottom += resize_ref.top;
|
||||
} else {
|
||||
resize_adjustments[n] = Option::from(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: resize_ref.top,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||
resize.top = 0;
|
||||
}
|
||||
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
|
||||
resize.top = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -818,7 +922,7 @@ fn recursive_fibonacci(
|
||||
right: resized.right,
|
||||
bottom: resized.bottom,
|
||||
}]
|
||||
} else if idx % 2 != 0 {
|
||||
} else if !idx.is_multiple_of(2) {
|
||||
let mut res = vec![Rect {
|
||||
left: resized.left,
|
||||
top: main_y,
|
||||
@@ -1115,6 +1219,37 @@ fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rec
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_scrolling_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
|
||||
if len <= 1 {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (i, rect) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
let is_leftmost = i == 0;
|
||||
let is_rightmost = i == len - 1;
|
||||
|
||||
resize_left(&mut result[i], rect.left);
|
||||
resize_right(&mut result[i], rect.right);
|
||||
resize_top(&mut result[i], rect.top);
|
||||
resize_bottom(&mut result[i], rect.bottom);
|
||||
|
||||
if !is_leftmost && rect.left != 0 {
|
||||
resize_right(&mut result[i - 1], rect.left);
|
||||
}
|
||||
|
||||
if !is_rightmost && rect.right != 0 {
|
||||
resize_left(&mut result[i + 1], rect.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn resize_left(rect: &mut Rect, resize: i32) {
|
||||
rect.left += resize / 2;
|
||||
rect.right += -resize / 2;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::config_generation::ApplicationConfiguration;
|
||||
use crate::config_generation::ApplicationOptions;
|
||||
use crate::config_generation::MatchingRule;
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -36,12 +36,12 @@ impl DerefMut for ApplicationSpecificConfiguration {
|
||||
}
|
||||
|
||||
impl ApplicationSpecificConfiguration {
|
||||
pub fn load(pathbuf: &PathBuf) -> Result<Self> {
|
||||
pub fn load(pathbuf: &PathBuf) -> eyre::Result<Self> {
|
||||
let content = std::fs::read_to_string(pathbuf)?;
|
||||
Ok(serde_json::from_str(&content)?)
|
||||
}
|
||||
|
||||
pub fn format(pathbuf: &PathBuf) -> Result<String> {
|
||||
pub fn format(pathbuf: &PathBuf) -> eyre::Result<String> {
|
||||
Ok(serde_json::to_string_pretty(&Self::load(pathbuf)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
@@ -53,41 +53,64 @@ impl ApplicationOptions {
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Rule for matching applications
|
||||
pub enum MatchingRule {
|
||||
/// Simple matching rule which must evaluate to true
|
||||
Simple(IdWithIdentifier),
|
||||
/// Composite matching rule where all conditions must evaluate to true
|
||||
Composite(Vec<IdWithIdentifier>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Rule for assigning applications to a workspace
|
||||
pub struct WorkspaceMatchingRule {
|
||||
/// Target monitor index
|
||||
pub monitor_index: usize,
|
||||
/// Target workspace index
|
||||
pub workspace_index: usize,
|
||||
/// Matching rule for the application
|
||||
pub matching_rule: MatchingRule,
|
||||
/// Whether to apply the rule only when the application is initially launched
|
||||
pub initial_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Rule for matching applications
|
||||
pub struct IdWithIdentifier {
|
||||
/// Kind of identifier to target
|
||||
pub kind: ApplicationIdentifier,
|
||||
/// Target identifier
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Matching strategy to use
|
||||
pub matching_strategy: Option<MatchingStrategy>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Strategy for matching identifiers
|
||||
pub enum MatchingStrategy {
|
||||
/// Should not be used, only kept for backward compatibility
|
||||
Legacy,
|
||||
/// Equals
|
||||
Equals,
|
||||
/// Starts With
|
||||
StartsWith,
|
||||
/// Ends With
|
||||
EndsWith,
|
||||
/// Contains
|
||||
Contains,
|
||||
/// Regex
|
||||
Regex,
|
||||
/// Does not end with
|
||||
DoesNotEndWith,
|
||||
/// Does not start with
|
||||
DoesNotStartWith,
|
||||
/// Does not equal
|
||||
DoesNotEqual,
|
||||
/// Does not contain
|
||||
DoesNotContain,
|
||||
}
|
||||
|
||||
@@ -142,11 +165,11 @@ impl ApplicationConfiguration {
|
||||
pub struct ApplicationConfigurationGenerator;
|
||||
|
||||
impl ApplicationConfigurationGenerator {
|
||||
pub fn load(content: &str) -> Result<Vec<ApplicationConfiguration>> {
|
||||
pub fn load(content: &str) -> eyre::Result<Vec<ApplicationConfiguration>> {
|
||||
Ok(serde_yaml::from_str(content)?)
|
||||
}
|
||||
|
||||
pub fn format(content: &str) -> Result<String> {
|
||||
pub fn format(content: &str) -> eyre::Result<String> {
|
||||
let mut cfgen = Self::load(content)?;
|
||||
for cfg in &mut cfgen {
|
||||
cfg.populate_default_matching_strategies();
|
||||
@@ -156,7 +179,10 @@ impl ApplicationConfigurationGenerator {
|
||||
Ok(serde_yaml::to_string(&cfgen)?)
|
||||
}
|
||||
|
||||
fn merge(base_content: &str, override_content: &str) -> Result<Vec<ApplicationConfiguration>> {
|
||||
fn merge(
|
||||
base_content: &str,
|
||||
override_content: &str,
|
||||
) -> eyre::Result<Vec<ApplicationConfiguration>> {
|
||||
let base_cfgen = Self::load(base_content)?;
|
||||
let override_cfgen = Self::load(override_content)?;
|
||||
|
||||
@@ -182,7 +208,7 @@ impl ApplicationConfigurationGenerator {
|
||||
pub fn generate_pwsh(
|
||||
base_content: &str,
|
||||
override_content: Option<&str>,
|
||||
) -> Result<Vec<String>> {
|
||||
) -> eyre::Result<Vec<String>> {
|
||||
let mut cfgen = if let Some(override_content) = override_content {
|
||||
Self::merge(base_content, override_content)?
|
||||
} else {
|
||||
@@ -233,7 +259,10 @@ impl ApplicationConfigurationGenerator {
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
pub fn generate_ahk(base_content: &str, override_content: Option<&str>) -> Result<Vec<String>> {
|
||||
pub fn generate_ahk(
|
||||
base_content: &str,
|
||||
override_content: Option<&str>,
|
||||
) -> eyre::Result<Vec<String>> {
|
||||
let mut cfgen = if let Some(override_content) = override_content {
|
||||
Self::merge(base_content, override_content)?
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::bail;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
@@ -5,12 +9,6 @@ use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::eyre::anyhow;
|
||||
use color_eyre::eyre::bail;
|
||||
use color_eyre::Result;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::Rect;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -32,7 +30,7 @@ impl DerefMut for CustomLayout {
|
||||
}
|
||||
|
||||
impl CustomLayout {
|
||||
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
pub fn from_path<P: AsRef<Path>>(path: P) -> eyre::Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let layout: Self = match path.extension() {
|
||||
Some(extension) if extension == "yaml" || extension == "yml" => {
|
||||
@@ -41,7 +39,7 @@ impl CustomLayout {
|
||||
Some(extension) if extension == "json" => {
|
||||
serde_json::from_reader(BufReader::new(File::open(path)?))?
|
||||
}
|
||||
_ => return Err(anyhow!("custom layouts must be json or yaml files")),
|
||||
_ => bail!("custom layouts must be json or yaml files"),
|
||||
};
|
||||
|
||||
if !layout.is_valid() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use clap::ValueEnum;
|
||||
use core::str::FromStr;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
@@ -8,22 +9,154 @@ use super::OperationDirection;
|
||||
use super::Rect;
|
||||
use super::Sizing;
|
||||
|
||||
pub fn deserialize_option_none_default_layout<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<DefaultLayout>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
if s == "None" {
|
||||
Ok(None)
|
||||
} else {
|
||||
<DefaultLayout as FromStr>::from_str(&s)
|
||||
.map(Some)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// A predefined komorebi layout
|
||||
pub enum DefaultLayout {
|
||||
/// BSP Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-------+-----+
|
||||
/// | | |
|
||||
/// | +--+--+
|
||||
/// | | |--|
|
||||
/// +-------+--+--+
|
||||
/// ```
|
||||
BSP,
|
||||
/// Columns Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +--+--+--+--+
|
||||
/// | | | | |
|
||||
/// | | | | |
|
||||
/// | | | | |
|
||||
/// +--+--+--+--+
|
||||
/// ```
|
||||
Columns,
|
||||
/// Rows Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-----------+
|
||||
/// |-----------|
|
||||
/// |-----------|
|
||||
/// |-----------|
|
||||
/// +-----------+
|
||||
/// ```
|
||||
Rows,
|
||||
/// Vertical Stack Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-------+-----+
|
||||
/// | | |
|
||||
/// | +-----+
|
||||
/// | | |
|
||||
/// +-------+-----+
|
||||
/// ```
|
||||
VerticalStack,
|
||||
/// Horizontal Stack Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +------+------+
|
||||
/// | |
|
||||
/// |------+------+
|
||||
/// | | |
|
||||
/// +------+------+
|
||||
/// ```
|
||||
HorizontalStack,
|
||||
/// Ultrawide Vertical Stack Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-----+-----------+-----+
|
||||
/// | | | |
|
||||
/// | | +-----+
|
||||
/// | | | |
|
||||
/// | | +-----+
|
||||
/// | | | |
|
||||
/// +-----+-----------+-----+
|
||||
/// ```
|
||||
UltrawideVerticalStack,
|
||||
/// Grid Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||
/// | | | | | | | | | | | | | | |
|
||||
/// | | | | | | | | | | | | | +---+
|
||||
/// +-----+-----+ | +---+---+ +---+---+---+ +---+---| |
|
||||
/// | | | | | | | | | | | | | +---+
|
||||
/// | | | | | | | | | | | | | | |
|
||||
/// +-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||
/// 4 windows 5 windows 6 windows 7 windows
|
||||
/// ```
|
||||
Grid,
|
||||
/// Right Main Vertical Stack Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-----+-------+
|
||||
/// | | |
|
||||
/// +-----+ |
|
||||
/// | | |
|
||||
/// +-----+-------+
|
||||
/// ```
|
||||
RightMainVerticalStack,
|
||||
/// Scrolling Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +--+--+--+--+--+--+
|
||||
/// | | | |
|
||||
/// | | | |
|
||||
/// | | | |
|
||||
/// +--+--+--+--+--+--+
|
||||
/// ```
|
||||
Scrolling,
|
||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Options for specific layouts
|
||||
pub struct LayoutOptions {
|
||||
/// Options related to the Scrolling layout
|
||||
pub scrolling: Option<ScrollingLayoutOptions>,
|
||||
/// Options related to the Grid layout
|
||||
pub grid: Option<GridLayoutOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Options for the Scrolling layout
|
||||
pub struct ScrollingLayoutOptions {
|
||||
/// Desired number of visible columns (default: 3)
|
||||
pub columns: usize,
|
||||
/// With an odd number of visible columns, keep the focused window column centered
|
||||
pub center_focused_column: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Options for the Grid layout
|
||||
pub struct GridLayoutOptions {
|
||||
/// Maximum number of rows per grid column
|
||||
pub rows: usize,
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
@@ -31,6 +164,7 @@ impl DefaultLayout {
|
||||
n if n > 1 => 1,
|
||||
_ => 0,
|
||||
},
|
||||
Self::Scrolling => 0,
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
@@ -53,6 +187,7 @@ impl DefaultLayout {
|
||||
_ => len.saturating_sub(1),
|
||||
},
|
||||
DefaultLayout::RightMainVerticalStack => 0,
|
||||
DefaultLayout::Scrolling => len.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +210,7 @@ impl DefaultLayout {
|
||||
| Self::RightMainVerticalStack
|
||||
| Self::HorizontalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::Scrolling
|
||||
) {
|
||||
return None;
|
||||
};
|
||||
@@ -169,13 +305,15 @@ impl DefaultLayout {
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
Self::RightMainVerticalStack => Self::Scrolling,
|
||||
Self::Scrolling => Self::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn cycle_previous(self) -> Self {
|
||||
match self {
|
||||
Self::Scrolling => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::DefaultLayout;
|
||||
use super::OperationDirection;
|
||||
use super::custom_layout::Column;
|
||||
use super::custom_layout::ColumnSplit;
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use super::custom_layout::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::OperationDirection;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
|
||||
pub trait Direction {
|
||||
fn index_in_direction(
|
||||
@@ -11,6 +12,7 @@ pub trait Direction {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize>;
|
||||
|
||||
fn is_valid_direction(
|
||||
@@ -18,30 +20,35 @@ pub trait Direction {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool;
|
||||
fn up_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
fn down_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
fn left_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
fn right_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
}
|
||||
|
||||
@@ -51,32 +58,53 @@ impl Direction for DefaultLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize> {
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.left_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.right_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.up_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.down_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -89,6 +117,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool {
|
||||
if count < 2 {
|
||||
return false;
|
||||
@@ -101,16 +130,18 @@ impl Direction for DefaultLayout {
|
||||
Self::Rows | Self::HorizontalStack => idx != 0,
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx > 2,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Down => match self {
|
||||
Self::BSP => idx != count - 1 && idx % 2 != 0,
|
||||
Self::BSP => idx != count - 1 && !idx.is_multiple_of(2),
|
||||
Self::Columns => false,
|
||||
Self::Rows => idx != count - 1,
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
|
||||
Self::HorizontalStack => idx == 0,
|
||||
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
Self::BSP => idx != 0,
|
||||
@@ -119,10 +150,11 @@ impl Direction for DefaultLayout {
|
||||
Self::Rows => false,
|
||||
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx != 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => idx != 0,
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
Self::BSP => idx % 2 == 0 && idx != count - 1,
|
||||
Self::BSP => idx.is_multiple_of(2) && idx != count - 1,
|
||||
Self::Columns => idx != count - 1,
|
||||
Self::Rows => false,
|
||||
Self::VerticalStack => idx == 0,
|
||||
@@ -132,7 +164,8 @@ impl Direction for DefaultLayout {
|
||||
2 => idx != 0,
|
||||
_ => idx < 2,
|
||||
},
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => idx != count - 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -142,10 +175,11 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
if idx.is_multiple_of(2) {
|
||||
idx - 1
|
||||
} else {
|
||||
idx - 2
|
||||
@@ -157,7 +191,8 @@ impl Direction for DefaultLayout {
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::RightMainVerticalStack => idx - 1,
|
||||
Self::HorizontalStack => 0,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +201,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP
|
||||
@@ -175,7 +211,8 @@ impl Direction for DefaultLayout {
|
||||
| Self::RightMainVerticalStack => idx + 1,
|
||||
Self::Columns => unreachable!(),
|
||||
Self::HorizontalStack => 1,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,10 +221,11 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
if idx % 2 == 0 {
|
||||
if idx.is_multiple_of(2) {
|
||||
idx - 2
|
||||
} else {
|
||||
idx - 1
|
||||
@@ -202,7 +240,8 @@ impl Direction for DefaultLayout {
|
||||
1 => unreachable!(),
|
||||
_ => 0,
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => idx - 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +250,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
|
||||
@@ -222,7 +262,8 @@ impl Direction for DefaultLayout {
|
||||
0 => 2,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => idx + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,21 +293,32 @@ struct GridTouchingEdges {
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn get_grid_item(idx: usize, count: usize) -> GridItem {
|
||||
let num_cols = (count as f32).sqrt().ceil() as usize;
|
||||
fn get_grid_item(idx: usize, count: usize, layout_options: Option<LayoutOptions>) -> GridItem {
|
||||
let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
|
||||
let num_cols = if let Some(rows) = row_constraint {
|
||||
((count as f32) / (rows as f32)).ceil() as i32
|
||||
} else {
|
||||
(count as f32).sqrt().ceil() as i32
|
||||
};
|
||||
|
||||
let mut iter = 0;
|
||||
|
||||
for col in 0..num_cols {
|
||||
let remaining_windows = count - iter;
|
||||
let remaining_windows = (count - iter) as i32;
|
||||
let remaining_columns = num_cols - col;
|
||||
let num_rows_in_this_col = remaining_windows / remaining_columns;
|
||||
|
||||
let num_rows_in_this_col = if let Some(rows) = row_constraint {
|
||||
(remaining_windows / remaining_columns).min(rows as i32)
|
||||
} else {
|
||||
remaining_windows / remaining_columns
|
||||
};
|
||||
|
||||
for row in 0..num_rows_in_this_col {
|
||||
if iter == idx {
|
||||
return GridItem {
|
||||
state: GridItemState::Valid,
|
||||
row: row + 1,
|
||||
num_rows: num_rows_in_this_col,
|
||||
row: (row + 1) as usize,
|
||||
num_rows: num_rows_in_this_col as usize,
|
||||
touching_edges: GridTouchingEdges {
|
||||
left: col == 0,
|
||||
right: col == num_cols - 1,
|
||||
@@ -293,8 +345,13 @@ fn get_grid_item(idx: usize, count: usize) -> GridItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_grid_edge(op_direction: OperationDirection, idx: usize, count: usize) -> bool {
|
||||
let item = get_grid_item(idx, count);
|
||||
fn is_grid_edge(
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool {
|
||||
let item = get_grid_item(idx, count, layout_options);
|
||||
|
||||
match item.state {
|
||||
GridItemState::Invalid => false,
|
||||
@@ -311,6 +368,7 @@ fn grid_neighbor(
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
let Some(op_direction) = op_direction else {
|
||||
return 0;
|
||||
@@ -320,11 +378,11 @@ fn grid_neighbor(
|
||||
return 0;
|
||||
};
|
||||
|
||||
let item = get_grid_item(idx, count);
|
||||
let item = get_grid_item(idx, count, layout_options);
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
let item_from_prev_col = get_grid_item(idx - item.row, count);
|
||||
let item_from_prev_col = get_grid_item(idx - item.row, count, layout_options);
|
||||
|
||||
if item.touching_edges.up && item.num_rows != item_from_prev_col.num_rows {
|
||||
return idx - (item.num_rows - 1);
|
||||
@@ -348,36 +406,42 @@ impl Direction for CustomLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize> {
|
||||
if count <= self.len() {
|
||||
return DefaultLayout::Columns.index_in_direction(op_direction, idx, count);
|
||||
return DefaultLayout::Columns.index_in_direction(
|
||||
op_direction,
|
||||
idx,
|
||||
count,
|
||||
layout_options,
|
||||
);
|
||||
}
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.left_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.right_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.up_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.down_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -390,9 +454,15 @@ impl Direction for CustomLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool {
|
||||
if count <= self.len() {
|
||||
return DefaultLayout::Columns.is_valid_direction(op_direction, idx, count);
|
||||
return DefaultLayout::Columns.is_valid_direction(
|
||||
op_direction,
|
||||
idx,
|
||||
count,
|
||||
layout_options,
|
||||
);
|
||||
}
|
||||
|
||||
match op_direction {
|
||||
@@ -436,6 +506,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
idx - 1
|
||||
}
|
||||
@@ -445,6 +516,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
idx + 1
|
||||
}
|
||||
@@ -454,6 +526,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
if column_idx - 1 == 0 {
|
||||
@@ -468,6 +541,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
self.first_container_idx(column_idx + 1)
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||
#![allow(deprecated)] // allow deprecated variants like HidingBehaviour::Hide to be used in derive macros
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
use crate::KomorebiTheme;
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
@@ -22,14 +24,14 @@ pub use custom_layout::ColumnSplitWithCapacity;
|
||||
pub use custom_layout::ColumnWidth;
|
||||
pub use custom_layout::CustomLayout;
|
||||
pub use cycle_direction::CycleDirection;
|
||||
pub use default_layout::DefaultLayout;
|
||||
pub use default_layout::*;
|
||||
pub use direction::Direction;
|
||||
pub use layout::Layout;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use pathext::replace_env_in_path;
|
||||
pub use pathext::resolve_option_hashmap_usize_path;
|
||||
pub use pathext::PathExt;
|
||||
pub use pathext::ResolvedPathBuf;
|
||||
pub use pathext::replace_env_in_path;
|
||||
pub use pathext::resolve_option_hashmap_usize_path;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
@@ -54,6 +56,8 @@ pub enum SocketMessage {
|
||||
// Window / Container Commands
|
||||
FocusWindow(OperationDirection),
|
||||
MoveWindow(OperationDirection),
|
||||
PreselectDirection(OperationDirection),
|
||||
CancelPreselect,
|
||||
CycleFocusWindow(CycleDirection),
|
||||
CycleMoveWindow(CycleDirection),
|
||||
StackWindow(OperationDirection),
|
||||
@@ -86,6 +90,7 @@ pub enum SocketMessage {
|
||||
Close,
|
||||
Minimize,
|
||||
Promote,
|
||||
PromoteSwap,
|
||||
PromoteFocus,
|
||||
PromoteWindow(OperationDirection),
|
||||
EagerFocus(String),
|
||||
@@ -108,6 +113,7 @@ pub enum SocketMessage {
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(DefaultLayout),
|
||||
CycleLayout(CycleDirection),
|
||||
ScrollingLayoutColumns(NonZeroUsize),
|
||||
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
FlipLayout(Axis),
|
||||
ToggleWorkspaceWindowContainerBehaviour,
|
||||
@@ -200,6 +206,7 @@ pub enum SocketMessage {
|
||||
StackbarFontFamily(Option<String>),
|
||||
WorkAreaOffset(Rect),
|
||||
MonitorWorkAreaOffset(usize, Rect),
|
||||
WorkspaceWorkAreaOffset(usize, usize, Rect),
|
||||
ToggleWindowBasedWorkAreaOffset,
|
||||
ResizeDelta(i32),
|
||||
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
@@ -245,7 +252,7 @@ pub enum SocketMessage {
|
||||
}
|
||||
|
||||
impl SocketMessage {
|
||||
pub fn as_bytes(&self) -> Result<Vec<u8>> {
|
||||
pub fn as_bytes(&self) -> eyre::Result<Vec<u8>> {
|
||||
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
@@ -253,7 +260,7 @@ impl SocketMessage {
|
||||
impl FromStr for SocketMessage {
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
fn from_str(s: &str) -> eyre::Result<Self, Self::Err> {
|
||||
serde_json::from_str(s)
|
||||
}
|
||||
}
|
||||
@@ -267,17 +274,24 @@ pub struct SubscribeOptions {
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Stackbar mode
|
||||
pub enum StackbarMode {
|
||||
/// Always show
|
||||
Always,
|
||||
/// Never show
|
||||
Never,
|
||||
/// Show on stack
|
||||
OnStack,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Starbar label
|
||||
pub enum StackbarLabel {
|
||||
#[default]
|
||||
/// Process name
|
||||
Process,
|
||||
/// Window title
|
||||
Title,
|
||||
}
|
||||
|
||||
@@ -285,6 +299,7 @@ pub enum StackbarLabel {
|
||||
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Border style
|
||||
pub enum BorderStyle {
|
||||
#[default]
|
||||
/// Use the system border style
|
||||
@@ -299,6 +314,7 @@ pub enum BorderStyle {
|
||||
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Border style
|
||||
pub enum BorderImplementation {
|
||||
#[default]
|
||||
/// Use the adjustable komorebi border implementation
|
||||
@@ -322,13 +338,20 @@ pub enum BorderImplementation {
|
||||
Hash,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Window kind
|
||||
pub enum WindowKind {
|
||||
/// Single window
|
||||
Single,
|
||||
/// Stack container
|
||||
Stack,
|
||||
/// Monocle container
|
||||
Monocle,
|
||||
#[default]
|
||||
/// Unfocused window
|
||||
Unfocused,
|
||||
/// Unfocused locked container
|
||||
UnfocusedLocked,
|
||||
/// Floating window
|
||||
Floating,
|
||||
}
|
||||
|
||||
@@ -341,6 +364,7 @@ pub enum StateQuery {
|
||||
FocusedWindowIndex,
|
||||
FocusedWorkspaceName,
|
||||
FocusedWorkspaceLayout,
|
||||
FocusedContainerKind,
|
||||
Version,
|
||||
}
|
||||
|
||||
@@ -348,41 +372,64 @@ pub enum StateQuery {
|
||||
Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Application identifier
|
||||
pub enum ApplicationIdentifier {
|
||||
/// Executable name
|
||||
#[serde(alias = "exe")]
|
||||
Exe,
|
||||
/// Class
|
||||
#[serde(alias = "class")]
|
||||
Class,
|
||||
#[serde(alias = "title")]
|
||||
/// Window title
|
||||
Title,
|
||||
/// Executable path
|
||||
#[serde(alias = "path")]
|
||||
Path,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Focus follows mouse implementation
|
||||
pub enum FocusFollowsMouseImplementation {
|
||||
/// A custom FFM implementation (slightly more CPU-intensive)
|
||||
/// Custom FFM implementation (slightly more CPU-intensive)
|
||||
Komorebi,
|
||||
/// The native (legacy) Windows FFM implementation
|
||||
/// Native (legacy) Windows FFM implementation
|
||||
Windows,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Window management behaviour
|
||||
pub struct WindowManagementBehaviour {
|
||||
/// The current WindowContainerBehaviour to be used
|
||||
/// 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,
|
||||
/// Determines if a new window should be spawned floating when on the floating layer and the
|
||||
/// floating layer behaviour is set to float. This value is always calculated when checking for
|
||||
/// the management behaviour on a specific workspace.
|
||||
pub floating_layer_override: bool,
|
||||
/// The floating layer behaviour to be used if the float override is being used
|
||||
pub floating_layer_behaviour: FloatingLayerBehaviour,
|
||||
/// The `Placement` to be used when toggling a window to float
|
||||
pub toggle_float_placement: Placement,
|
||||
/// The `Placement` to be used when spawning a window on the floating layer with the
|
||||
/// `FloatingLayerBehaviour` set to `FloatingLayerBehaviour::Float`
|
||||
pub floating_layer_placement: Placement,
|
||||
/// The `Placement` to be used when spawning a window with float override active
|
||||
pub float_override_placement: Placement,
|
||||
/// The `Placement` to be used when spawning a window that matches a `floating_applications` rule
|
||||
pub float_rule_placement: Placement,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Window container behaviour when a new window is opened
|
||||
pub enum WindowContainerBehaviour {
|
||||
/// Create a new container for each new window
|
||||
#[default]
|
||||
@@ -395,18 +442,63 @@ pub enum WindowContainerBehaviour {
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Floating layer behaviour when a new window is opened
|
||||
pub enum FloatingLayerBehaviour {
|
||||
/// Tile new windows (unless they match a float rule)
|
||||
/// Tile new windows (unless they match a float rule or float override is active)
|
||||
#[default]
|
||||
Tile,
|
||||
/// Float new windows
|
||||
Float,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Placement behaviour for floating windows
|
||||
pub enum Placement {
|
||||
/// Does not change the size or position of the window
|
||||
#[default]
|
||||
None,
|
||||
/// Center the window without changing the size
|
||||
Center,
|
||||
/// Center the window and resize it according to the `AspectRatio`
|
||||
CenterAndResize,
|
||||
}
|
||||
|
||||
impl FloatingLayerBehaviour {
|
||||
pub fn should_float(&self) -> bool {
|
||||
match self {
|
||||
FloatingLayerBehaviour::Tile => false,
|
||||
FloatingLayerBehaviour::Float => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
pub fn should_center(&self) -> bool {
|
||||
match self {
|
||||
Placement::None => false,
|
||||
Placement::Center | Placement::CenterAndResize => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_resize(&self) -> bool {
|
||||
match self {
|
||||
Placement::None | Placement::Center => false,
|
||||
Placement::CenterAndResize => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Move behaviour when the operation works across a monitor boundary
|
||||
pub enum MoveBehaviour {
|
||||
/// Swap the window container with the window container at the edge of the adjacent monitor
|
||||
#[default]
|
||||
Swap,
|
||||
/// Insert the window container into the focused workspace on the adjacent monitor
|
||||
Insert,
|
||||
@@ -414,39 +506,52 @@ pub enum MoveBehaviour {
|
||||
NoOp,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Behaviour when an action would cross a monitor boundary
|
||||
pub enum CrossBoundaryBehaviour {
|
||||
/// Attempt to perform actions across a workspace boundary
|
||||
Workspace,
|
||||
/// Attempt to perform actions across a monitor boundary
|
||||
#[default]
|
||||
Monitor,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Window hiding behaviour
|
||||
pub enum HidingBehaviour {
|
||||
/// Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
/// END OF LIFE FEATURE: Use the `SW_HIDE` flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
#[deprecated(note = "End of life feature")]
|
||||
Hide,
|
||||
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
/// 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
|
||||
Cloak,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Operation behaviour for temporarily unmanaged and floating windows
|
||||
pub enum OperationBehaviour {
|
||||
/// Process komorebic commands on temporarily unmanaged/floated windows
|
||||
/// Process commands on temporarily unmanaged/floated windows
|
||||
#[default]
|
||||
Op,
|
||||
/// Ignore komorebic commands on temporarily unmanaged/floated windows
|
||||
/// Ignore commands on temporarily unmanaged/floated windows
|
||||
NoOp,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Sizing
|
||||
pub enum Sizing {
|
||||
/// Increase
|
||||
Increase,
|
||||
/// Decrease
|
||||
Decrease,
|
||||
}
|
||||
|
||||
@@ -466,6 +571,19 @@ impl Sizing {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Window handling behaviour
|
||||
pub enum WindowHandlingBehaviour {
|
||||
#[default]
|
||||
/// Synchronous
|
||||
Sync,
|
||||
/// Asynchronous
|
||||
Async,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -473,7 +591,9 @@ mod tests {
|
||||
#[test]
|
||||
fn deserializes() {
|
||||
// Set a variable for testing
|
||||
std::env::set_var("VAR", "VALUE");
|
||||
unsafe {
|
||||
std::env::set_var("VAR", "VALUE");
|
||||
}
|
||||
|
||||
let json = r#"{"type":"WorkspaceLayoutCustomRule","content":[0,0,0,"/path/%VAR%/d"]}"#;
|
||||
let message: SocketMessage = serde_json::from_str(json).unwrap();
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use super::Axis;
|
||||
use super::direction::Direction;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use clap::ValueEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use super::direction::Direction;
|
||||
use super::Axis;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum OperationDirection {
|
||||
@@ -57,7 +57,8 @@ impl OperationDirection {
|
||||
layout_flip: Option<Axis>,
|
||||
idx: usize,
|
||||
len: NonZeroUsize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize> {
|
||||
layout.index_in_direction(self.flip(layout_flip), idx, len.get())
|
||||
layout.index_in_direction(self.flip(layout_flip), idx, len.get(), layout_options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
@@ -10,9 +9,9 @@ use serde::Serialize;
|
||||
|
||||
/// Path extension trait
|
||||
pub trait PathExt {
|
||||
/// Resolve environment variables components in a path.
|
||||
/// Resolve environment variable components in a path.
|
||||
///
|
||||
/// Resolves the follwing formats:
|
||||
/// Resolves the following formats:
|
||||
/// - CMD: `%variable%`
|
||||
/// - PowerShell: `$Env:variable`
|
||||
/// - Bash: `$variable`.
|
||||
@@ -58,7 +57,7 @@ impl<P: AsRef<Path>> PathExt for P {
|
||||
// if component is a variable, get the value from the environment
|
||||
if let Some(var) = var {
|
||||
let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
|
||||
if let Some(value) = env::var_os(var) {
|
||||
if let Some(value) = std::env::var_os(var) {
|
||||
out.push(value);
|
||||
continue;
|
||||
}
|
||||
@@ -122,13 +121,16 @@ impl<'de> serde_with::DeserializeAs<'de, PathBuf> for ResolvedPathBuf {
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl serde_with::schemars_0_8::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
|
||||
fn schema_name() -> String {
|
||||
"PathBuf".to_owned()
|
||||
impl serde_with::schemars_1::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
std::borrow::Cow::Borrowed("PathBuf")
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
<PathBuf as schemars::JsonSchema>::json_schema(gen)
|
||||
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
schemars::json_schema!({
|
||||
"type": "string",
|
||||
"description": "A file system path. Environment variables like %VAR%, $Env:VAR, or $VAR are automatically resolved."
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +167,9 @@ mod tests {
|
||||
#[test]
|
||||
fn resolves_env_vars() {
|
||||
// Set a variable for testing
|
||||
std::env::set_var("VAR", "VALUE");
|
||||
unsafe {
|
||||
std::env::set_var("VAR", "VALUE");
|
||||
}
|
||||
|
||||
// %VAR% format
|
||||
assert_eq!(resolve("/path/%VAR%/d"), expected("/path/VALUE/d"));
|
||||
@@ -183,7 +187,9 @@ mod tests {
|
||||
assert_eq!(resolve("/path/$ASD/to/d"), expected("/path/$ASD/to/d"));
|
||||
|
||||
// Set a $env:USERPROFILE variable for testing
|
||||
std::env::set_var("USERPROFILE", "C:\\Users\\user");
|
||||
unsafe {
|
||||
std::env::set_var("USERPROFILE", "C:\\Users\\user");
|
||||
}
|
||||
|
||||
// ~ and $HOME should be replaced with $Env:USERPROFILE
|
||||
assert_eq!(resolve("~"), expected("C:\\Users\\user"));
|
||||
|
||||
@@ -4,14 +4,15 @@ use windows::Win32::Foundation::RECT;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Rectangle dimensions
|
||||
pub struct Rect {
|
||||
/// The left point in a Win32 Rect
|
||||
/// Left point of the rectangle
|
||||
pub left: i32,
|
||||
/// The top point in a Win32 Rect
|
||||
/// Top point of the rectangle
|
||||
pub top: i32,
|
||||
/// The right point in a Win32 Rect
|
||||
/// Width of the recentangle (from the left point)
|
||||
pub right: i32,
|
||||
/// The bottom point in a Win32 Rect
|
||||
/// Height of the rectangle (from the top point)
|
||||
pub bottom: i32,
|
||||
}
|
||||
|
||||
@@ -41,6 +42,10 @@ impl Rect {
|
||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||
self.right == rhs.right && self.bottom == rhs.bottom
|
||||
}
|
||||
|
||||
pub fn has_same_position_as(&self, rhs: &Self) -> bool {
|
||||
self.left == rhs.left && self.top == rhs.top
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
|
||||
@@ -44,13 +44,15 @@ pub fn send_notification(hwnd: isize) {
|
||||
}
|
||||
|
||||
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
|
||||
std::thread::spawn(move || loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match handle_notifications(wm.clone()) {
|
||||
Ok(()) => {
|
||||
tracing::warn!("restarting finished thread");
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("restarting failed thread: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ pub mod ring;
|
||||
pub mod container;
|
||||
pub mod core;
|
||||
pub mod focus_manager;
|
||||
pub mod locked_deque;
|
||||
pub mod lockable_sequence;
|
||||
pub mod monitor;
|
||||
pub mod monitor_reconciliator;
|
||||
pub mod process_command;
|
||||
@@ -16,7 +16,9 @@ pub mod process_event;
|
||||
pub mod process_movement;
|
||||
pub mod reaper;
|
||||
pub mod set_window_position;
|
||||
pub mod splash;
|
||||
pub mod stackbar_manager;
|
||||
pub mod state;
|
||||
pub mod static_config;
|
||||
pub mod styles;
|
||||
pub mod theme_manager;
|
||||
@@ -39,12 +41,12 @@ use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use core::*;
|
||||
pub use komorebi_themes::colour::*;
|
||||
@@ -62,17 +64,19 @@ use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use crate::core::config_generation::WorkspaceMatchingRule;
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use os_info::Version;
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::RwLock;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use state::State;
|
||||
use uds_windows::UnixStream;
|
||||
use which::which;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
use winreg::RegKey;
|
||||
use winreg::enums::HKEY_CURRENT_USER;
|
||||
|
||||
lazy_static! {
|
||||
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
|
||||
@@ -159,7 +163,14 @@ lazy_static! {
|
||||
})
|
||||
]));
|
||||
static ref SESSION_FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(vec![
|
||||
MatchingRule::Simple(IdWithIdentifier {
|
||||
kind: ApplicationIdentifier::Exe,
|
||||
id: String::from("komorebi-shortcuts.exe"),
|
||||
matching_strategy: Option::from(MatchingStrategy::Equals),
|
||||
})
|
||||
|
||||
]));
|
||||
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
|
||||
"Chrome_RenderWidgetHostHWND".to_string(),
|
||||
]));
|
||||
@@ -192,8 +203,7 @@ lazy_static! {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
|
||||
@@ -204,11 +214,10 @@ lazy_static! {
|
||||
pub static ref AHK_EXE: String = {
|
||||
let mut ahk: String = String::from("autohotkey.exe");
|
||||
|
||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
|
||||
if which(&komorebi_ahk_exe).is_ok() {
|
||||
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE")
|
||||
&& which(&komorebi_ahk_exe).is_ok() {
|
||||
ahk = komorebi_ahk_exe;
|
||||
}
|
||||
}
|
||||
|
||||
ahk
|
||||
};
|
||||
@@ -227,11 +236,17 @@ lazy_static! {
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
static ref FLOATING_WINDOW_TOGGLE_ASPECT_RATIO: Arc<Mutex<AspectRatio>> = Arc::new(Mutex::new(AspectRatio::Predefined(PredefinedAspectRatio::Widescreen)));
|
||||
|
||||
static ref CURRENT_VIRTUAL_DESKTOP: Arc<Mutex<Option<Vec<u8>>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
pub static DEFAULT_WORKSPACE_LAYOUT: AtomicCell<Option<DefaultLayout>> =
|
||||
AtomicCell::new(Some(DefaultLayout::BSP));
|
||||
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
|
||||
pub static DEFAULT_RESIZE_DELTA: i32 = 50;
|
||||
|
||||
pub static DEFAULT_MOUSE_FOLLOWS_FOCUS: bool = true;
|
||||
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
|
||||
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
|
||||
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
|
||||
@@ -240,8 +255,34 @@ pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub static SLOW_APPLICATION_COMPENSATION_TIME: AtomicU64 = AtomicU64::new(20);
|
||||
|
||||
pub static WINDOW_HANDLING_BEHAVIOUR: AtomicCell<WindowHandlingBehaviour> =
|
||||
AtomicCell::new(WindowHandlingBehaviour::Sync);
|
||||
|
||||
shadow_rs::shadow!(build);
|
||||
|
||||
pub const PUBLIC_KEY: [u8; 32] = [
|
||||
0x5a, 0x69, 0x4a, 0xe1, 0x3c, 0x4b, 0xc8, 0x4e, 0xc3, 0x79, 0x0f, 0xab, 0x27, 0x6b, 0x7e, 0xdd,
|
||||
0x6b, 0x39, 0x6f, 0xa2, 0xc3, 0x9f, 0x3d, 0x48, 0xf2, 0x72, 0x56, 0x41, 0x1b, 0xc8, 0x08, 0xdb,
|
||||
];
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct License {
|
||||
#[serde(rename = "hasValidSubscription")]
|
||||
pub has_valid_subscription: bool,
|
||||
pub timestamp: i64,
|
||||
#[serde(rename = "currentEndPeriod")]
|
||||
pub current_end_period: Option<i64>,
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// A trait for types that can be marked as locked or unlocked.
|
||||
pub trait Lockable {
|
||||
/// Returns `true` if the item is locked.
|
||||
fn locked(&self) -> bool;
|
||||
/// Sets the locked state of the item.
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
@@ -286,23 +327,34 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
current
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
WindowManager(WindowManagerEvent),
|
||||
Socket(SocketMessage),
|
||||
Monitor(MonitorNotification),
|
||||
VirtualDesktop(VirtualDesktopNotification),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum VirtualDesktopNotification {
|
||||
EnteredAssociatedVirtualDesktop,
|
||||
LeftAssociatedVirtualDesktop,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
|
||||
pub fn notify_subscribers(
|
||||
notification: Notification,
|
||||
state_has_been_modified: bool,
|
||||
) -> eyre::Result<()> {
|
||||
let is_override_event = matches!(
|
||||
notification.event,
|
||||
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
|
||||
@@ -383,7 +435,7 @@ pub fn notify_subscribers(notification: Notification, state_has_been_modified: b
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_configuration() -> Result<()> {
|
||||
pub fn load_configuration() -> eyre::Result<()> {
|
||||
let config_pwsh = HOME_DIR.join("komorebi.ps1");
|
||||
let config_ahk = HOME_DIR.join("komorebi.ahk");
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user