Compare commits

..

1 Commits

Author SHA1 Message Date
LGUG2Z 7fed31bc54 wip use cached monitor idx when handling newly spawned windows 2025-03-31 15:40:59 -07:00
154 changed files with 16231 additions and 33140 deletions
-1
View File
@@ -1 +0,0 @@
use flake
-2
View File
@@ -55,7 +55,6 @@ body:
label: Hotkey Configuration label: Hotkey Configuration
description: > description: >
Please provide your whkdrc or komorebi.ahk hotkey configuration file Please provide your whkdrc or komorebi.ahk hotkey configuration file
render: shell
- type: textarea - type: textarea
validations: validations:
required: true required: true
@@ -63,4 +62,3 @@ body:
label: Output of komorebic check label: Output of komorebic check
description: > description: >
Please provide the output of `komorebic check` Please provide the output of `komorebic check`
render: shell
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Check and close feature issues - name: Check and close feature issues
uses: actions/github-script@v8 uses: actions/github-script@v7
with: with:
script: | script: |
const issue = context.payload.issue; const issue = context.payload.issue;
+11 -12
View File
@@ -13,15 +13,15 @@ on:
- hotfix/* - hotfix/*
tags: tags:
- v* - v*
# schedule: schedule:
# - cron: "30 0 * * 0" # Every day at 00:30 UTC - cron: "30 0 * * 0" # Every day at 00:30 UTC
workflow_dispatch: workflow_dispatch:
jobs: jobs:
cargo-deny: cargo-deny:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: EmbarkStudios/cargo-deny-action@v2 - uses: EmbarkStudios/cargo-deny-action@v2
@@ -43,11 +43,10 @@ jobs:
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- run: rustup toolchain install stable --profile minimal - 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 - run: rustup toolchain install nightly --allow-downgrade -c rustfmt
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
@@ -65,7 +64,7 @@ jobs:
- run: | - run: |
cargo install cargo-wix cargo install cargo-wix
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }} cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v6 - uses: actions/upload-artifact@v4
with: with:
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }} name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
path: | path: |
@@ -82,12 +81,12 @@ jobs:
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- shell: bash - shell: bash
run: echo "VERSION=nightly" >> $GITHUB_ENV run: echo "VERSION=nightly" >> $GITHUB_ENV
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v4
- run: | - 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 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 Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
@@ -129,14 +128,14 @@ jobs:
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- shell: bash - shell: bash
run: | run: |
TAG=${{ github.event.release.tag_name }} TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v4
- run: | - 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 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 Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
@@ -171,14 +170,14 @@ jobs:
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- shell: bash - shell: bash
run: | run: |
TAG=${{ github.ref_name }} TAG=${{ github.ref_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v7 - uses: actions/download-artifact@v4
- run: | - 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 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 Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
-6
View File
@@ -6,9 +6,3 @@ dummy.go
komorebic/applications.yaml komorebic/applications.yaml
komorebic/applications.json komorebic/applications.json
/.vs /.vs
/bar-schema
/komorebi-schema
/.wrangler
/.xwin-cache
result
/.direnv
Generated
+1568 -2998
View File
File diff suppressed because it is too large Load Diff
+43 -56
View File
@@ -2,15 +2,13 @@
resolver = "2" resolver = "2"
members = [ members = [
"komorebi", "komorebi",
"komorebi-client", "komorebi-client",
"komorebi-gui", "komorebi-gui",
"komorebi-layouts", "komorebic",
"komorebic", "komorebic-no-console",
"komorebic-no-console", "komorebi-bar",
"komorebi-bar", "komorebi-themes"
"komorebi-themes",
"komorebi-shortcuts",
] ]
[workspace.dependencies] [workspace.dependencies]
@@ -20,12 +18,12 @@ chrono = "0.4"
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
crossbeam-utils = "0.8" crossbeam-utils = "0.8"
color-eyre = "0.6" color-eyre = "0.6"
eframe = "0.33" eframe = "0.31"
egui_extras = "0.33" egui_extras = "0.31"
dirs = "6" dirs = "6"
dunce = "1" dunce = "1"
hotwatch = "0.5" hotwatch = "0.5"
schemars = "1.1" schemars = "0.8"
lazy_static = "1" lazy_static = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" } serde_json = { package = "serde_json_lenient", version = "0.2" }
@@ -34,55 +32,44 @@ strum = { version = "0.27", features = ["derive"] }
tracing = "0.1" tracing = "0.1"
tracing-appender = "0.2" tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
parking_lot = "0.12"
paste = "1" paste = "1"
sysinfo = "0.37" sysinfo = "0.33"
uds_windows = "1" uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" } win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "a28c6559a9de2f92c142a714947a9b081776caca" }
windows-numerics = { version = "0.3" } windows-numerics = { version = "0.2" }
windows-implement = { version = "0.60" } windows-implement = { version = "0.60" }
windows-interface = { version = "0.59" } windows-interface = { version = "0.59" }
windows-core = { version = "0.62" } windows-core = { version = "0.61" }
shadow-rs = "1" shadow-rs = "1"
which = "8" which = "7"
[workspace.dependencies.windows] [workspace.dependencies.windows]
version = "0.62" version = "0.61"
features = [ features = [
"Foundation_Numerics", "Foundation_Numerics",
"Win32_Devices", "Win32_Devices",
"Win32_Devices_Display", "Win32_Devices_Display",
"Win32_System_Com", "Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray "Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation", "Win32_Foundation",
"Win32_Globalization", "Win32_Globalization",
"Win32_Graphics_Dwm", "Win32_Graphics_Dwm",
"Win32_Graphics_Gdi", "Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D", "Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common", "Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common", "Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader", "Win32_System_LibraryLoader",
"Win32_System_Power", "Win32_System_Power",
"Win32_System_RemoteDesktop", "Win32_System_RemoteDesktop",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_UI_Accessibility", "Win32_UI_Accessibility",
"Win32_UI_HiDpi", "Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell", "Win32_UI_Shell",
"Win32_UI_Shell_Common", "Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices", "Win32_System_SystemServices",
"Win32_System_WindowsProgramming", "Win32_System_WindowsProgramming",
"Media", "Media",
"Media_Control", "Media_Control"
] ]
[profile.release-opt]
inherits = "release"
lto = true
panic = "abort"
codegen-units = 1
strip = true
[workspace.metadata.crane]
name = "komorebi-workspace"
+1 -31
View File
@@ -29,36 +29,6 @@ Tiling Window Management for Windows.
![screenshot](https://user-images.githubusercontent.com/13164844/184027064-f5a6cec2-2865-4d65-a549-a1f1da589abf.png) ![screenshot](https://user-images.githubusercontent.com/13164844/184027064-f5a6cec2-2865-4d65-a549-a1f1da589abf.png)
## 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
komorebi for Mac lives [here](https://github.com/LGUG2Z/komorebi-for-mac) :)
## Overview ## Overview
_komorebi_ is a tiling window manager that works as an extension to Microsoft's _komorebi_ is a tiling window manager that works as an extension to Microsoft's
@@ -424,7 +394,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. Below is a simple example of how to use `komorebi-client` in a basic Rust application.
```rust ```rust
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.39"} // komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.35"}
use anyhow::Result; use anyhow::Result;
use komorebi_client::Notification; use komorebi_client::Notification;
-432
View File
@@ -1,432 +0,0 @@
#!/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())
+32 -64
View File
@@ -1,8 +1,8 @@
[graph] [graph]
targets = [ targets = [
"x86_64-pc-windows-msvc", "x86_64-pc-windows-msvc",
"i686-pc-windows-msvc", "i686-pc-windows-msvc",
"aarch64-pc-windows-msvc", "aarch64-pc-windows-msvc",
] ]
all-features = false all-features = false
no-default-features = false no-default-features = false
@@ -12,87 +12,64 @@ feature-depth = 1
[advisories] [advisories]
ignore = [ ignore = [
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" }, { 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-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] [licenses]
allow = [ allow = [
"0BSD", "0BSD",
"Apache-2.0", "Apache-2.0",
"Apache-2.0 WITH LLVM-exception", "Artistic-2.0",
"Artistic-2.0", "BSD-2-Clause",
"BSD-2-Clause", "BSD-3-Clause",
"BSD-3-Clause", "BSL-1.0",
"BSL-1.0", "CC0-1.0",
"CC0-1.0", "ISC",
"ISC", "MIT",
"MIT", "MIT-0",
"MIT-0", "MPL-2.0",
"MPL-2.0", "OFL-1.1",
"OFL-1.1", "Ubuntu-font-1.0",
"Ubuntu-font-1.0", "Unicode-3.0",
"Unicode-3.0", "Zlib",
"Zlib", "LicenseRef-Komorebi-1.0"
"LicenseRef-Komorebi-2.0",
] ]
confidence-threshold = 0.8 confidence-threshold = 0.8
[[licenses.clarify]] [[licenses.clarify]]
crate = "komorebi" crate = "komorebi"
expression = "LicenseRef-Komorebi-2.0" expression = "LicenseRef-Komorebi-1.0"
license-files = [] license-files = []
[[licenses.clarify]] [[licenses.clarify]]
crate = "komorebi-client" crate = "komorebi-client"
expression = "LicenseRef-Komorebi-2.0" expression = "LicenseRef-Komorebi-1.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-layouts"
expression = "LicenseRef-Komorebi-2.0"
license-files = [] license-files = []
[[licenses.clarify]] [[licenses.clarify]]
crate = "komorebic" crate = "komorebic"
expression = "LicenseRef-Komorebi-2.0" expression = "LicenseRef-Komorebi-1.0"
license-files = [] license-files = []
[[licenses.clarify]] [[licenses.clarify]]
crate = "komorebic-no-console" crate = "komorebic-no-console"
expression = "LicenseRef-Komorebi-2.0" expression = "LicenseRef-Komorebi-1.0"
license-files = [] license-files = []
[[licenses.clarify]] [[licenses.clarify]]
crate = "komorebi-themes" crate = "komorebi-themes"
expression = "LicenseRef-Komorebi-2.0" expression = "LicenseRef-Komorebi-1.0"
license-files = [] license-files = []
[[licenses.clarify]] [[licenses.clarify]]
crate = "komorebi-gui" crate = "komorebi-gui"
expression = "LicenseRef-Komorebi-2.0" expression = "LicenseRef-Komorebi-1.0"
license-files = [] license-files = []
[[licenses.clarify]] [[licenses.clarify]]
crate = "komorebi-bar" crate = "komorebi-bar"
expression = "LicenseRef-Komorebi-2.0" expression = "LicenseRef-Komorebi-1.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 = [] license-files = []
[[licenses.clarify]] [[licenses.clarify]]
@@ -100,11 +77,6 @@ crate = "base16-egui-themes"
expression = "MIT" expression = "MIT"
license-files = [] license-files = []
[[licenses.clarify]]
crate = "win32-display-data"
expression = "0BSD"
license-files = []
[bans] [bans]
multiple-versions = "allow" multiple-versions = "allow"
wildcards = "allow" wildcards = "allow"
@@ -117,12 +89,8 @@ unknown-registry = "deny"
unknown-git = "deny" unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"] allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = [ allow-git = [
"https://github.com/LGUG2Z/base16-egui-themes", "https://github.com/LGUG2Z/base16-egui-themes",
"https://github.com/LGUG2Z/windows-icons", "https://github.com/LGUG2Z/catppuccin-egui",
"https://github.com/LGUG2Z/win32-display-data", "https://github.com/LGUG2Z/windows-icons",
"https://github.com/LGUG2Z/flavours", "https://github.com/LGUG2Z/win32-display-data",
"https://github.com/LGUG2Z/base16_color_scheme",
"https://github.com/LGUG2Z/whkd",
"https://github.com/LGUG2Z/catppuccin-egui",
"https://github.com/amPerl/egui-phosphor",
] ]
+391 -607
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

+3 -2
View File
@@ -10,8 +10,9 @@ Options:
Desired ease function for animation Desired ease function for animation
[default: linear] [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, [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-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] 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> -a, --animation-type <ANIMATION_TYPE>
Animation type to apply the style to. If not specified, sets global style Animation type to apply the style to. If not specified, sets global style
-12
View File
@@ -1,12 +0,0 @@
# 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
```
+1 -1
View File
@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
Arguments: Arguments:
<DEFAULT_LAYOUT> <DEFAULT_LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling] [possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options: Options:
-h, --help -h, --help
-12
View File
@@ -1,12 +0,0 @@
# clear-session-float-rules
```
Clear all session float rules
Usage: komorebic.exe clear-session-float-rules
Options:
-h, --help
Print help
```
-12
View File
@@ -1,12 +0,0 @@
# data-directory
```
Show the path to komorebi's data directory in %LOCALAPPDATA%
Usage: komorebic.exe data-directory
Options:
-h, --help
Print help
```
+3
View File
@@ -12,6 +12,9 @@ Options:
--whkd --whkd
Enable autostart of whkd Enable autostart of whkd
--ahk
Enable autostart of ahk
--bar --bar
Enable autostart of komorebi-bar Enable autostart of komorebi-bar
+3
View File
@@ -9,6 +9,9 @@ Options:
--whkd --whkd
Kill whkd if it is running as a background process Kill whkd if it is running as a background process
--ahk
Kill ahk if it is running as a background process
--bar --bar
Kill komorebi-bar if it is running as a background process Kill komorebi-bar if it is running as a background process
-16
View File
@@ -1,16 +0,0 @@
# 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
```
+1 -1
View File
@@ -13,7 +13,7 @@ Arguments:
The number of window containers on-screen required to trigger this layout rule The number of window containers on-screen required to trigger this layout rule
<LAYOUT> <LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling] [possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options: Options:
-h, --help -h, --help
+1 -1
View File
@@ -10,7 +10,7 @@ Arguments:
Target workspace name Target workspace name
<VALUE> <VALUE>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling] [possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options: Options:
-h, --help -h, --help
-16
View File
@@ -1,16 +0,0 @@
# 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
View File
@@ -1,12 +0,0 @@
# 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 -1
View File
@@ -1,7 +1,7 @@
# promote # promote
``` ```
Promote the focused window to the largest tile via container removal and re-insertion Promote the focused window to the top of the tree
Usage: komorebic.exe promote Usage: komorebic.exe promote
+1 -1
View File
@@ -7,7 +7,7 @@ Usage: komorebic.exe query <STATE_QUERY>
Arguments: Arguments:
<STATE_QUERY> <STATE_QUERY>
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, focused-container-kind, version] [possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name]
Options: Options:
-h, --help -h, --help
-16
View File
@@ -1,16 +0,0 @@
# 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
```
-12
View File
@@ -1,12 +0,0 @@
# session-float-rule
```
Add a rule to float the foreground window for the rest of this session
Usage: komorebic.exe session-float-rule
Options:
-h, --help
Print help
```
-12
View File
@@ -1,12 +0,0 @@
# session-float-rules
```
Show all session float rules
Usage: komorebic.exe session-float-rules
Options:
-h, --help
Print help
```
+3
View File
@@ -18,6 +18,9 @@ Options:
--whkd --whkd
Start whkd in a background process Start whkd in a background process
--ahk
Start autohotkey configuration file
--bar --bar
Start komorebi-bar in a background process Start komorebi-bar in a background process
+3
View File
@@ -9,6 +9,9 @@ Options:
--whkd --whkd
Stop whkd if it is running as a background process Stop whkd if it is running as a background process
--ahk
Stop ahk if it is running as a background process
--bar --bar
Stop komorebi-bar if it is running as a background process Stop komorebi-bar if it is running as a background process
+1 -1
View File
@@ -1,7 +1,7 @@
# toggle-pause # toggle-pause
``` ```
Toggle the paused state for all window tiling Toggle window tiling on the focused workspace
Usage: komorebic.exe toggle-pause Usage: komorebic.exe toggle-pause
-12
View File
@@ -1,12 +0,0 @@
# toggle-shortcuts
```
Toggle the komorebi-shortcuts helper
Usage: komorebic.exe toggle-shortcuts
Options:
-h, --help
Print help
```
+2 -1
View File
@@ -1,7 +1,8 @@
# toggle-workspace-float-override # toggle-workspace-float-override
``` ```
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace previously it takes the opposite of the global value Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace previously it takes
the opposite of the global value
Usage: komorebic.exe toggle-workspace-float-override Usage: komorebic.exe toggle-workspace-float-override
+1 -1
View File
@@ -8,7 +8,7 @@ Usage: komorebic.exe window-hiding-behaviour <HIDING_BEHAVIOUR>
Arguments: Arguments:
<HIDING_BEHAVIOUR> <HIDING_BEHAVIOUR>
Possible values: Possible values:
- hide: END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps) - hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching) - minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces - cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
+1 -1
View File
@@ -16,7 +16,7 @@ Arguments:
The number of window containers on-screen required to trigger this layout rule The number of window containers on-screen required to trigger this layout rule
<LAYOUT> <LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling] [possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options: Options:
-h, --help -h, --help
+1 -1
View File
@@ -13,7 +13,7 @@ Arguments:
Workspace index on the specified monitor (zero-indexed) Workspace index on the specified monitor (zero-indexed)
<VALUE> <VALUE>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling] [possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options: Options:
-h, --help -h, --help
-31
View File
@@ -1,31 +0,0 @@
# 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
```
+1 -4
View File
@@ -6,10 +6,7 @@ defined in the `komorebi.json` configuration file.
```json ```json
{ {
"animation": { "animation": {
"enabled": true, "enabled": true
"duration": 250,
"fps": 60,
"style": "EaseOutSine"
} }
} }
``` ```
-200
View File
@@ -1,200 +0,0 @@
# Layout Ratios
With `komorebi` you can customize the split ratios for various layouts using
`column_ratios` and `row_ratios` in the `layout_options` configuration.
## Before and After
BSP layout example:
**Before** (default 50/50 splits):
![Before layout ratios](../assets/layout-ratios_before.png)
**After** (with `column_ratios: [0.7]` and `row_ratios: [0.6]`):
![After layout ratios](../assets/layout-ratios_after.png)
## Configuration
```json
{
"monitors": [
{
"workspaces": [
{
"name": "main",
"layout_options": {
"column_ratios": [0.3, 0.4],
"row_ratios": [0.4, 0.3]
}
}
]
}
]
}
```
You can specify up to 5 ratio values (defined by `MAX_RATIOS` constant). Each value should be between 0.1 and 0.9
(defined by `MIN_RATIO` and `MAX_RATIO` constants). Values outside this range are automatically clamped.
Columns or rows without a specified ratio will share the remaining space equally.
## Usage by Layout
| Layout | `column_ratios` | `row_ratios` |
|--------|-----------------|--------------|
| **Columns** | Width of each column | - |
| **Rows** | - | Height of each row |
| **Grid** | Width of each column (rows are equal height) | - |
| **BSP** | `[0]` as horizontal split ratio | `[0]` as vertical split ratio |
| **VerticalStack** | `[0]` as primary column width | Stack row heights |
| **RightMainVerticalStack** | `[0]` as primary column width | Stack row heights |
| **HorizontalStack** | Stack column widths | `[0]` as primary row height |
| **UltrawideVerticalStack** | `[0]` center, `[1]` left column | Tertiary stack row heights |
## Examples
### Columns Layout with Custom Widths
Create 3 columns with 30%, 40%, and 30% widths:
```json
{
"layout_options": {
"column_ratios": [0.3, 0.4]
}
}
```
Note: The third column automatically gets the remaining 30%.
### Rows Layout with Custom Heights
Create 3 rows with 20%, 50%, and 30% heights:
```json
{
"layout_options": {
"row_ratios": [0.2, 0.5]
}
}
```
Note: The third row automatically gets the remaining 30%.
### Grid Layout with Custom Column Widths
Grid with custom column widths (rows within each column are always equal height):
```json
{
"layout_options": {
"column_ratios": [0.4, 0.6]
}
}
```
Note: The Grid layout only supports `column_ratios`. Rows within each column are always
divided equally because the number of rows per column varies dynamically based on window count.
### VerticalStack with Custom Ratios
Primary column takes 60% width, and the stack rows are split 30%/70%:
```json
{
"layout_options": {
"column_ratios": [0.6],
"row_ratios": [0.3]
}
}
```
Note: The second row automatically gets the remaining 70%.
### HorizontalStack with Custom Ratios
Primary row takes 70% height, and the stack columns are split 40%/60%:
```json
{
"layout_options": {
"row_ratios": [0.7],
"column_ratios": [0.4]
}
}
```
Note: The second column automatically gets the remaining 60%.
### UltrawideVerticalStack with Custom Ratios
Center column at 50%, left column at 25% (remaining 25% goes to tertiary stack),
with tertiary rows split 40%/60%:
```json
{
"layout_options": {
"column_ratios": [0.5, 0.25],
"row_ratios": [0.4]
}
}
```
Note: The second row automatically gets the remaining 60%.
### BSP Layout with Custom Split Ratios
Use separate ratios for horizontal (left/right) and vertical (top/bottom) splits:
```json
{
"layout_options": {
"column_ratios": [0.6],
"row_ratios": [0.3]
}
}
```
- `column_ratios[0]`: Controls all horizontal splits (left window gets 60%, right gets 40%)
- `row_ratios[0]`: Controls all vertical splits (top window gets 30%, bottom gets 70%)
Note: BSP only uses the first value (`[0]`) from each ratio array. This single ratio is applied
consistently to all splits of that type throughout the layout. Additional values in the arrays are ignored.
## Notes
- Ratios are clamped between 0.1 and 0.9 (prevents zero-sized windows and ensures space for other windows)
- Default ratio is 0.5 (50%) when not specified, except for UltrawideVerticalStack secondary column which defaults to 0.25 (25%)
- Ratios are applied **progressively** - a ratio is only used when there are more windows to place after the current one
- The **last window always takes the remaining space**, regardless of defined ratios
- **Ratios that would sum to 100% or more are automatically truncated** at config load time to ensure there's always space for additional windows
- Unspecified ratios default to sharing the remaining space equally
- You only need to specify the ratios you want to customize; trailing values can be omitted
## Progressive Ratio Behavior
Ratios are applied progressively as windows are added. For example, with `row_ratios: [0.3, 0.5]` in a VerticalStack:
| Windows in Stack | Row Heights |
|------------------|-------------|
| 1 | 100% |
| 2 | 30%, 70% (remainder) |
| 3 | 30%, 50%, 20% (remainder) |
| 4 | 30%, 50%, 10%, 10% (remainder split equally) |
| 5 | 30%, 50%, 6.67%, 6.67%, 6.67% |
## Automatic Ratio Truncation
When ratios sum to 100% (or more), they are automatically truncated at config load time.
For example, if you configure `column_ratios: [0.4, 0.3, 0.3]` (sums to 100%), the last ratio (0.3) is automatically removed, resulting in effectively `[0.4, 0.3]`. This ensures there's always remaining space for the last window.
| Configured Ratios | Effective Ratios | Reason |
|-------------------|------------------|--------|
| `[0.3, 0.4]` | `[0.3, 0.4]` | Sum is 0.7, below 1.0 |
| `[0.4, 0.3, 0.3]` | `[0.4, 0.3]` | Sum would be 1.0, last ratio truncated |
| `[0.5, 0.5]` | `[0.5]` | Sum would be 1.0, last ratio truncated |
| `[0.6, 0.5]` | `[0.6]` | Sum would exceed 1.0, last ratio truncated |
This ensures the layout always fills 100% of the available space and new windows are never placed outside the visible area.
+1 -8
View File
@@ -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. 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. However, if you do this it is important to be aware of a few things.
@@ -393,13 +393,6 @@ 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 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. 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 ### 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 * If you are using a laptop connected to one monitor at work and a different one at home, the work monitor and the home
+5 -1
View File
@@ -8,8 +8,12 @@ configuration file.
```json ```json
{ {
"default_workspace_padding": 0, "default_workspace_padding": 0,
"default_container_padding": -1, "default_container_padding": 0,
"border_width": 0,
"border_offset": -1
} }
``` ```
A restart of `komorebi` is required after changing these settings.
[![Watch the tutorial video](https://img.youtube.com/vi/6QYLao953XE/hqdefault.jpg)](https://www.youtube.com/watch?v=6QYLao953XE) [![Watch the tutorial video](https://img.youtube.com/vi/6QYLao953XE/hqdefault.jpg)](https://www.youtube.com/watch?v=6QYLao953XE)
@@ -0,0 +1,17 @@
# 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>"
}
}
```
-19
View File
@@ -16,19 +16,6 @@ the example files have been downloaded. For most new users this will be in the
komorebic quickstart 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`, With the example configurations downloaded, you can now start `komorebi`,
`komorebi-bar` and `whkd`. `komorebi-bar` and `whkd`.
@@ -36,9 +23,6 @@ With the example configurations downloaded, you can now start `komorebi`,
komorebic start --whkd --bar 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 ## komorebi.json
The example window manager configuration sets some sane defaults and provides The example window manager configuration sets some sane defaults and provides
@@ -202,9 +186,6 @@ 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 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. `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" %} {% include "./whkdrc.sample" %}
``` ```
-1
View File
@@ -120,7 +120,6 @@ cargo +stable install --path komorebic --locked
cargo +stable install --path komorebic-no-console --locked cargo +stable install --path komorebic-no-console --locked
cargo +stable install --path komorebi-gui --locked cargo +stable install --path komorebi-gui --locked
cargo +stable install --path komorebi-bar --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 If the binaries have been built and added to your `$PATH` correctly, you should
+8 -7
View File
@@ -1,5 +1,6 @@
{ {
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.bar.json", "$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.35/schema.bar.json",
"monitor": 0,
"font_family": "JetBrains Mono", "font_family": "JetBrains Mono",
"theme": { "theme": {
"palette": "Base16", "palette": "Base16",
@@ -31,24 +32,24 @@
}, },
{ {
"Media": { "Media": {
"enable": false "enable": true
} }
}, },
{ {
"Storage": { "Storage": {
"enable": false "enable": true
} }
}, },
{ {
"Memory": { "Memory": {
"enable": false "enable": true
} }
}, },
{ {
"Network": { "Network": {
"enable": false, "enable": true,
"show_activity": true, "show_total_data_transmitted": true,
"show_total_activity": true "show_network_activity": true
} }
}, },
{ {
+8 -1
View File
@@ -1,5 +1,5 @@
{ {
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.json", "$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.35/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json", "app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
"window_hiding_behaviour": "Cloak", "window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert", "cross_monitor_move_behaviour": "Insert",
@@ -14,6 +14,13 @@
"unfocused_border": "Base03", "unfocused_border": "Base03",
"bar_accent": "Base0D" "bar_accent": "Base0D"
}, },
"stackbar": {
"height": 40,
"mode": "OnStack",
"tabs": {
"width": 300
}
},
"monitors": [ "monitors": [
{ {
"workspaces": [ "workspaces": [
-17
View File
@@ -132,20 +132,3 @@ running `komorebic stop` and `komorebic start`.
We can see the _komorebi_ state is no longer associated with the previous We can see the _komorebi_ state is no longer associated with the previous
device: `null`, suggesting an issue when the display resumes from a suspended device: `null`, suggesting an issue when the display resumes from a suspended
state. state.
## Komorebi Bar does not render transparency on Nvidia GPUs
Users with Nvidia GPUs may have issues with transparency on the Komorebi Bar.
To solve this the user can do the following:
- 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.
-2
View File
@@ -5,8 +5,6 @@
alt + o : taskkill /f /im whkd.exe; Start-Process whkd -WindowStyle hidden # if shell is pwsh / powershell alt + o : taskkill /f /im whkd.exe; Start-Process whkd -WindowStyle hidden # if shell is pwsh / powershell
alt + shift + o : komorebic reload-configuration alt + shift + o : komorebic reload-configuration
alt + i : komorebic toggle-shortcuts
# App shortcuts - these require shell to be pwsh / powershell # App shortcuts - these require shell to be pwsh / powershell
# The apps will be focused if open, or launched if not open # The apps will be focused if open, or launched if not open
# alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox } # alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox }
Generated
-179
View File
@@ -1,179 +0,0 @@
{
"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
View File
@@ -1,360 +0,0 @@
{
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;
};
};
};
};
}
+14 -36
View File
@@ -15,9 +15,6 @@ fmt:
prettier --write .github/FUNDING.yml prettier --write .github/FUNDING.yml
prettier --write .github/workflows/windows.yaml prettier --write .github/workflows/windows.yaml
fix:
cargo clippy --fix --allow-dirty
install-targets *targets: install-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ } "{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
@@ -31,10 +28,10 @@ install-target-with-jsonschema target:
cargo +stable install --path {{ target }} --locked cargo +stable install --path {{ target }} --locked
install: install:
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
install-with-jsonschema: install-with-jsonschema:
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
build-targets *targets: build-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just build-target $_ } "{{ targets }}" -split ' ' | ForEach-Object { just build-target $_ }
@@ -43,7 +40,7 @@ build-target target:
cargo +stable build --package {{ target }} --locked --release --no-default-features cargo +stable build --package {{ target }} --locked --release --no-default-features
build: build:
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
copy-target target: copy-target target:
cp .\target\release\{{ target }}.exe $Env:USERPROFILE\.cargo\bin cp .\target\release\{{ target }}.exe $Env:USERPROFILE\.cargo\bin
@@ -55,7 +52,7 @@ wpm target:
just build-target {{ target }} && wpmctl stop {{ target }}; just copy-target {{ target }} && wpmctl start {{ target }} just build-target {{ target }} && wpmctl stop {{ target }}; just copy-target {{ target }} && wpmctl start {{ target }}
copy: copy:
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui komorebi-shortcuts just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
run target: run target:
cargo +stable run --bin {{ target }} --locked --no-default-features cargo +stable run --bin {{ target }} --locked --no-default-features
@@ -75,38 +72,19 @@ trace target $RUST_LOG="trace":
deadlock $RUST_LOG="trace": deadlock $RUST_LOG="trace":
cargo +stable run --bin komorebi --locked --no-default-features --features deadlock_detection cargo +stable run --bin komorebi --locked --no-default-features --features deadlock_detection
docgen starlight: docgen:
rm {{ starlight }}/src/data/cli/windows/*.md cargo run --package komorebic -- docgen
cargo run --package komorebic -- docgen --output {{ starlight }}/src/data/cli/windows Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
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: jsonschema:
cargo run --package komorebic -- static-config-schema > schema.json cargo run --package komorebic -- static-config-schema > schema.json
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
cargo run --package komorebi-bar -- --schema > schema.bar.json cargo run --package komorebi-bar -- --schema > schema.bar.json
version := `cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "komorebi") | .version'` # this part is run in a nix shell because python is a nightmare
schemagen:
schemapub: rm -rf static-config-docs bar-config-docs
rm -Force komorebi-schema mkdir -p static-config-docs bar-config-docs
mkdir -Force komorebi-schema generate-schema-doc ./schema.json --config template_name=js_offline --config minify=false ./static-config-docs/
cp schema.json komorebi-schema/komorebi.{{ version }}.schema.json generate-schema-doc ./schema.bar.json --config template_name=js_offline --config minify=false ./bar-config-docs/
cp schema.bar.json komorebi-schema/komorebi.bar.{{ version }}.schema.json mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html
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
+8 -14
View File
@@ -1,13 +1,13 @@
[package] [package]
name = "komorebi-bar" name = "komorebi-bar"
version = "0.1.40" version = "0.1.36"
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
komorebi-client = { path = "../komorebi-client", default-features = false } komorebi-client = { path = "../komorebi-client" }
komorebi-themes = { path = "../komorebi-themes", default-features = false } komorebi-themes = { path = "../komorebi-themes" }
chrono-tz = { workspace = true } chrono-tz = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
@@ -17,18 +17,17 @@ crossbeam-channel = { workspace = true }
dirs = { workspace = true } dirs = { workspace = true }
dunce = { workspace = true } dunce = { workspace = true }
eframe = { workspace = true } eframe = { workspace = true }
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" } egui-phosphor = "0.9"
font-loader = "0.11" font-loader = "0.11"
hotwatch = { workspace = true } hotwatch = { workspace = true }
image = "0.25" image = "0.25"
lazy_static = { workspace = true } lazy_static = { workspace = true }
netdev = "0.40" netdev = "0.33"
num = "0.4" num = "0.4"
num-derive = "0.4" num-derive = "0.4"
num-traits = "0.2" num-traits = "0.2"
parking_lot = { workspace = true }
random_word = { version = "0.5", features = ["en"] } random_word = { version = "0.5", features = ["en"] }
reqwest = { version = "0.13", features = ["blocking"] } reqwest = { version = "0.12", features = ["blocking"] }
schemars = { workspace = true, optional = true } schemars = { workspace = true, optional = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
@@ -36,7 +35,6 @@ starship-battery = "0.10"
sysinfo = { workspace = true } sysinfo = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
which = { workspace = true }
windows = { workspace = true } windows = { workspace = true }
windows-core = { workspace = true } windows-core = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" } windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" }
@@ -44,8 +42,4 @@ windows-icons-fallback = { package = "windows-icons", git = "https://github.com/
[features] [features]
default = ["schemars"] default = ["schemars"]
schemars = [ schemars = ["dep:schemars"]
"dep:schemars",
"komorebi-client/default",
"komorebi-themes/default",
]
+166 -546
View File
@@ -1,28 +1,25 @@
use crate::AUTO_SELECT_FILL_COLOUR; use crate::config::get_individual_spacing;
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::KomobarConfig;
use crate::config::KomobarTheme; use crate::config::KomobarTheme;
use crate::config::MonitorConfigOrIndex; use crate::config::MonitorConfigOrIndex;
use crate::config::Position; use crate::config::Position;
use crate::config::PositionConfig; use crate::config::PositionConfig;
use crate::config::get_individual_spacing;
use crate::process_hwnd; use crate::process_hwnd;
use crate::render::Color32Ext; use crate::render::Color32Ext;
use crate::render::Grouping; use crate::render::Grouping;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::render::RenderExt; use crate::render::RenderExt;
use crate::widgets::komorebi::Komorebi; use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::MonitorInfo; use crate::widgets::komorebi::KomorebiNotificationState;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use crate::widgets::widget::WidgetConfig; use crate::widgets::widget::WidgetConfig;
use color_eyre::eyre; use crate::KomorebiEvent;
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 crossbeam_channel::Receiver; use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError; use crossbeam_channel::TryRecvError;
use eframe::egui::Align; use eframe::egui::Align;
@@ -39,7 +36,6 @@ use eframe::egui::Frame;
use eframe::egui::Id; use eframe::egui::Id;
use eframe::egui::Layout; use eframe::egui::Layout;
use eframe::egui::Margin; use eframe::egui::Margin;
use eframe::egui::PointerButton;
use eframe::egui::Rgba; use eframe::egui::Rgba;
use eframe::egui::Style; use eframe::egui::Style;
use eframe::egui::TextStyle; use eframe::egui::TextStyle;
@@ -47,102 +43,20 @@ use eframe::egui::Vec2;
use eframe::egui::Visuals; use eframe::egui::Visuals;
use font_loader::system_fonts; use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder; use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::Colour; use komorebi_client::KomorebiTheme;
use komorebi_client::MonitorNotification; use komorebi_client::MonitorNotification;
use komorebi_client::NotificationEvent; use komorebi_client::NotificationEvent;
use komorebi_client::PathExt;
use komorebi_client::SocketMessage; use komorebi_client::SocketMessage;
use komorebi_client::VirtualDesktopNotification;
use komorebi_themes::Base16Wrapper;
use komorebi_themes::Catppuccin;
use komorebi_themes::KomobarThemeBase16;
use komorebi_themes::KomobarThemeCatppuccin;
use komorebi_themes::KomobarThemeCustom;
use komorebi_themes::catppuccin_egui; use komorebi_themes::catppuccin_egui;
use lazy_static::lazy_static; use komorebi_themes::Base16Value;
use parking_lot::Mutex; use komorebi_themes::Catppuccin;
use komorebi_themes::CatppuccinValue;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; 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::path::PathBuf;
use std::process::ChildStdin;
use std::process::Command;
use std::process::Stdio;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::Arc;
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 struct Komobar {
pub hwnd: Option<isize>, pub hwnd: Option<isize>,
@@ -150,7 +64,7 @@ pub struct Komobar {
pub disabled: bool, pub disabled: bool,
pub config: KomobarConfig, pub config: KomobarConfig,
pub render_config: Rc<RefCell<RenderConfig>>, pub render_config: Rc<RefCell<RenderConfig>>,
pub monitor_info: Option<Rc<RefCell<MonitorInfo>>>, pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>, pub left_widgets: Vec<Box<dyn BarWidget>>,
pub center_widgets: Vec<Box<dyn BarWidget>>, pub center_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>, pub right_widgets: Vec<Box<dyn BarWidget>>,
@@ -162,18 +76,6 @@ pub struct Komobar {
pub size_rect: komorebi_client::Rect, pub size_rect: komorebi_client::Rect,
pub work_area_offset: komorebi_client::Rect, pub work_area_offset: komorebi_client::Rect,
applied_theme_on_first_frame: bool, 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( pub fn apply_theme(
@@ -185,86 +87,75 @@ pub fn apply_theme(
grouping: Option<Grouping>, grouping: Option<Grouping>,
render_config: Rc<RefCell<RenderConfig>>, render_config: Rc<RefCell<RenderConfig>>,
) { ) {
let (auto_select_fill, auto_select_text) = match theme { match theme {
KomobarTheme::Catppuccin(KomobarThemeCatppuccin { KomobarTheme::Catppuccin {
name: catppuccin, name: catppuccin,
accent: catppuccin_value, accent: catppuccin_value,
auto_select_fill: catppuccin_auto_select_fill, } => match catppuccin {
auto_select_text: catppuccin_auto_select_text, Catppuccin::Frappe => {
}) => { catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
match catppuccin { let catppuccin_value = catppuccin_value.unwrap_or_default();
Catppuccin::Frappe => { let accent = catppuccin_value.color32(catppuccin.as_theme());
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| { ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent; style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent; style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent; style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None; style.visuals.override_text_color = None;
}); });
bg_color.replace(catppuccin_egui::FRAPPE.base); bg_color.replace(catppuccin_egui::FRAPPE.base);
}
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
} }
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
( ctx.style_mut(|style| {
catppuccin_auto_select_fill.map(|c| c.color32(catppuccin.as_theme())), style.visuals.selection.stroke.color = accent;
catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())), style.visuals.widgets.hovered.fg_stroke.color = accent;
) style.visuals.widgets.active.fg_stroke.color = accent;
} style.visuals.override_text_color = None;
KomobarTheme::Base16(KomobarThemeBase16 { });
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
},
KomobarTheme::Base16 {
name: base16, name: base16,
accent: base16_value, accent: base16_value,
auto_select_fill: base16_auto_select_fill, } => {
auto_select_text: base16_auto_select_text,
}) => {
ctx.set_style(base16.style()); ctx.set_style(base16.style());
let base16_value = base16_value.unwrap_or_default(); let base16_value = base16_value.unwrap_or_default();
let accent = base16_value.color32(Base16Wrapper::Base16(base16)); let accent = base16_value.color32(base16);
ctx.style_mut(|style| { ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent; style.visuals.selection.stroke.color = accent;
@@ -273,46 +164,8 @@ pub fn apply_theme(
}); });
bg_color.replace(base16.background()); bg_color.replace(base16.background());
(
base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Base16(base16))),
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))),
)
} }
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();
let accent = base16_value.color32(Base16Wrapper::Custom(colours.clone()));
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
});
bg_color.replace(background);
(
base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))),
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))),
)
}
};
AUTO_SELECT_FILL_COLOUR.store(
auto_select_fill.map_or(0, |c| Colour::from(c).into()),
Ordering::SeqCst,
);
AUTO_SELECT_TEXT_COLOUR.store(
auto_select_text.map_or(0, |c| Colour::from(c).into()),
Ordering::SeqCst,
);
// Apply transparency_alpha // Apply transparency_alpha
let theme_color = *bg_color.borrow(); let theme_color = *bg_color.borrow();
@@ -322,15 +175,16 @@ pub fn apply_theme(
// apply rounding to the widgets // apply rounding to the widgets
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) = if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) =
&grouping &grouping
&& let Some(rounding) = config.rounding
{ {
ctx.style_mut(|style| { if let Some(rounding) = config.rounding {
style.visuals.widgets.noninteractive.corner_radius = rounding.into(); ctx.style_mut(|style| {
style.visuals.widgets.inactive.corner_radius = rounding.into(); style.visuals.widgets.noninteractive.corner_radius = rounding.into();
style.visuals.widgets.hovered.corner_radius = rounding.into(); style.visuals.widgets.inactive.corner_radius = rounding.into();
style.visuals.widgets.active.corner_radius = rounding.into(); style.visuals.widgets.hovered.corner_radius = rounding.into();
style.visuals.widgets.open.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 // Update RenderConfig's background_color so that widgets will have the new color
@@ -341,7 +195,7 @@ impl Komobar {
pub fn apply_config( pub fn apply_config(
&mut self, &mut self,
ctx: &Context, ctx: &Context,
previous_monitor_info: Option<Rc<RefCell<MonitorInfo>>>, previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
) { ) {
MAX_LABEL_WIDTH.store( MAX_LABEL_WIDTH.store(
self.config.max_label_width.unwrap_or(400.0) as i32, self.config.max_label_width.unwrap_or(400.0) as i32,
@@ -370,7 +224,7 @@ impl Komobar {
self.config.icon_scale, self.config.icon_scale,
)); ));
let mut monitor_info = previous_monitor_info; let mut komorebi_notification_state = previous_notification_state;
let mut komorebi_widgets = Vec::new(); let mut komorebi_widgets = Vec::new();
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() { for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
@@ -422,18 +276,19 @@ impl Komobar {
komorebi_widgets komorebi_widgets
.into_iter() .into_iter()
.for_each(|(mut widget, idx, side)| { .for_each(|(mut widget, idx, side)| {
match monitor_info { match komorebi_notification_state {
None => { None => {
monitor_info = Some(widget.monitor_info.clone()); komorebi_notification_state =
Some(widget.komorebi_notification_state.clone());
} }
Some(ref previous) => { Some(ref previous) => {
if widget.workspaces.is_some() { if widget.workspaces.is_some_and(|w| w.enable) {
previous previous.borrow_mut().update_from_config(
.borrow_mut() &widget.komorebi_notification_state.borrow(),
.update_from_self(&widget.monitor_info.borrow()); );
} }
widget.monitor_info = previous.clone(); widget.komorebi_notification_state = previous.clone();
} }
} }
@@ -453,25 +308,20 @@ impl Komobar {
self.right_widgets = right_widgets; self.right_widgets = right_widgets;
let (usr_monitor_index, config_work_area_offset) = match &self.config.monitor { let (usr_monitor_index, config_work_area_offset) = match &self.config.monitor {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => { MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
(monitor_config.index, monitor_config.work_area_offset) (monitor_config.index, monitor_config.work_area_offset)
} }
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None), MonitorConfigOrIndex::Index(idx) => (*idx, None),
None => (0, None),
}; };
let monitor_index = self.komorebi_notification_state.as_ref().and_then(|state| {
let mapped_info = self.monitor_info.as_ref().map(|info| { state
let monitor = info.borrow(); .borrow()
( .monitor_usr_idx_map
monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(), .get(&usr_monitor_index)
monitor.mouse_follows_focus, .copied()
)
}); });
if let Some(info) = mapped_info { self.monitor_index = monitor_index;
self.monitor_index = info.0;
self.mouse_follows_focus = info.1;
}
if let Some(monitor_index) = self.monitor_index { if let Some(monitor_index) = self.monitor_index {
if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset) if let (prev_rect, Some(new_rect)) = (&self.work_area_offset, &config_work_area_offset)
@@ -522,51 +372,17 @@ impl Komobar {
} }
} }
} }
} else if self.monitor_info.is_some() && !self.disabled { } else if self.komorebi_notification_state.is_some() && !self.disabled {
tracing::warn!( tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
"couldn't find the monitor index of this bar! Disabling the bar until the monitor connects..."
);
self.disabled = true; self.disabled = true;
} else { } else {
tracing::warn!( 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.");
"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; 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"); tracing::info!("widget configuration options applied");
self.monitor_info = monitor_info; self.komorebi_notification_state = komorebi_notification_state;
} }
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not /// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
@@ -603,9 +419,7 @@ impl Komobar {
end.x -= margin.left + margin.right; end.x -= margin.left + margin.right;
if end.y == 0.0 { if end.y == 0.0 {
tracing::warn!( 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")
"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 { self.size_rect = komorebi_client::Rect {
@@ -617,11 +431,11 @@ impl Komobar {
} }
fn try_apply_theme(&mut self, ctx: &Context) { fn try_apply_theme(&mut self, ctx: &Context) {
match &self.config.theme { match self.config.theme {
Some(theme) => { Some(theme) => {
apply_theme( apply_theme(
ctx, ctx,
theme.clone(), theme,
self.bg_color.clone(), self.bg_color.clone(),
self.bg_color_with_alpha.clone(), self.bg_color_with_alpha.clone(),
self.config.transparency_alpha, self.config.transparency_alpha,
@@ -633,15 +447,13 @@ impl Komobar {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else( let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"), |_| dirs::home_dir().expect("there is no home directory"),
|home_path| { |home_path| {
let home = home_path.replace_env(); let home = PathBuf::from(&home_path);
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
);
home
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
}, },
); );
@@ -660,6 +472,21 @@ impl Komobar {
bar_grouping, bar_grouping,
self.render_config.clone(), self.render_config.clone(),
); );
let stack_accent = match theme {
KomorebiTheme::Catppuccin {
name, stack_border, ..
} => stack_border
.unwrap_or(CatppuccinValue::Green)
.color32(name.as_theme()),
KomorebiTheme::Base16 {
name, stack_border, ..
} => stack_border.unwrap_or(Base16Value::Base0B).color32(name),
};
if let Some(state) = &self.komorebi_notification_state {
state.borrow_mut().stack_accent = Some(stack_accent);
}
} }
} }
Err(_) => { Err(_) => {
@@ -672,16 +499,17 @@ impl Komobar {
| Grouping::Alignment(config) | Grouping::Alignment(config)
| Grouping::Widget(config), | Grouping::Widget(config),
) = &bar_grouping ) = &bar_grouping
&& let Some(rounding) = config.rounding
{ {
ctx.style_mut(|style| { if let Some(rounding) = config.rounding {
style.visuals.widgets.noninteractive.corner_radius = ctx.style_mut(|style| {
rounding.into(); style.visuals.widgets.noninteractive.corner_radius =
style.visuals.widgets.inactive.corner_radius = rounding.into(); rounding.into();
style.visuals.widgets.hovered.corner_radius = rounding.into(); style.visuals.widgets.inactive.corner_radius = rounding.into();
style.visuals.widgets.active.corner_radius = rounding.into(); style.visuals.widgets.hovered.corner_radius = rounding.into();
style.visuals.widgets.open.corner_radius = rounding.into(); style.visuals.widgets.active.corner_radius = rounding.into();
}); style.visuals.widgets.open.corner_radius = rounding.into();
});
}
} }
} }
} }
@@ -701,7 +529,7 @@ impl Komobar {
disabled: false, disabled: false,
config, config,
render_config: Rc::new(RefCell::new(RenderConfig::new())), render_config: Rc::new(RefCell::new(RenderConfig::new())),
monitor_info: None, komorebi_notification_state: None,
left_widgets: vec![], left_widgets: vec![],
center_widgets: vec![], center_widgets: vec![],
right_widgets: vec![], right_widgets: vec![],
@@ -713,16 +541,6 @@ impl Komobar {
size_rect: komorebi_client::Rect::default(), size_rect: komorebi_client::Rect::default(),
work_area_offset: komorebi_client::Rect::default(), work_area_offset: komorebi_client::Rect::default(),
applied_theme_on_first_frame: false, 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); komobar.apply_config(&cc.egui_ctx, None);
@@ -847,12 +665,12 @@ impl eframe::App for Komobar {
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) { 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.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
self.apply_config(ctx, self.monitor_info.clone()); self.apply_config(ctx, self.komorebi_notification_state.clone());
} }
if let Ok(updated_config) = self.rx_config.try_recv() { if let Ok(updated_config) = self.rx_config.try_recv() {
self.config = updated_config; self.config = updated_config;
self.apply_config(ctx, self.monitor_info.clone()); self.apply_config(ctx, self.komorebi_notification_state.clone());
} }
match self.rx_gui.try_recv() { match self.rx_gui.try_recv() {
@@ -867,41 +685,13 @@ impl eframe::App for Komobar {
Ok(KomorebiEvent::Notification(notification)) => { Ok(KomorebiEvent::Notification(notification)) => {
let state = &notification.state; let state = &notification.state;
let usr_monitor_index = match &self.config.monitor { let usr_monitor_index = match &self.config.monitor {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => { MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
monitor_config.index MonitorConfigOrIndex::Index(idx) => *idx,
}
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
None => 0,
}; };
let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied(); let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied();
self.monitor_index = monitor_index; self.monitor_index = monitor_index;
let mut should_apply_config = false; 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() if self.monitor_index.is_none()
|| self || self
.monitor_index .monitor_index
@@ -946,9 +736,9 @@ impl eframe::App for Komobar {
) { ) {
let monitor_index = self.monitor_index.expect("should have a monitor index"); 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; should_apply_config = true;
} }
@@ -959,7 +749,7 @@ impl eframe::App for Komobar {
// Check if monitor coordinates/size has changed // Check if monitor coordinates/size has changed
if let Some(monitor_index) = self.monitor_index { 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 top = MONITOR_TOP.load(Ordering::SeqCst);
let left = MONITOR_LEFT.load(Ordering::SeqCst); let left = MONITOR_LEFT.load(Ordering::SeqCst);
let right = MONITOR_RIGHT.load(Ordering::SeqCst); let right = MONITOR_RIGHT.load(Ordering::SeqCst);
@@ -969,38 +759,36 @@ impl eframe::App for Komobar {
bottom: monitor_size.bottom, bottom: monitor_size.bottom,
right, right,
}; };
if monitor_size != rect { if *monitor_size != rect {
tracing::info!( tracing::info!(
"Monitor coordinates/size has changed, storing new coordinates: {:#?}", "Monitor coordinates/size has changed, storing new coordinates: {:#?}",
monitor_size monitor_size
); );
self.update_monitor_coordinates(&monitor_size); self.update_monitor_coordinates(monitor_size);
should_apply_config = true; should_apply_config = true;
} }
} }
if let Some(monitor_info) = &self.monitor_info { if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
monitor_info.borrow_mut().update( komorebi_notification_state
self.monitor_index, .borrow_mut()
notification.state, .handle_notification(
self.render_config.borrow().show_all_icons, ctx,
); self.monitor_index,
handle_notification( notification,
ctx, self.bg_color.clone(),
notification.event, self.bg_color_with_alpha.clone(),
self.bg_color.clone(), self.config.transparency_alpha,
self.bg_color_with_alpha.clone(), self.config.grouping,
self.config.transparency_alpha, self.config.theme,
self.config.grouping, self.render_config.clone(),
self.config.theme.clone(), );
self.render_config.clone(),
);
} }
if should_apply_config { if should_apply_config {
self.apply_config(ctx, self.monitor_info.clone()); self.apply_config(ctx, self.komorebi_notification_state.clone());
// Reposition the Bar // Reposition the Bar
self.position_bar(); self.position_bar();
@@ -1082,111 +870,6 @@ impl eframe::App for Komobar {
let frame = render_config.change_frame_on_bar(frame, &ctx.style()); let frame = render_config.change_frame_on_bar(frame, &ctx.style());
CentralPanel::default().frame(frame).show(ctx, |ui| { 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 // Apply grouping logic for the bar as a whole
let area_frame = if let Some(frame) = &self.config.frame { let area_frame = if let Some(frame) = &self.config.frame {
Frame::NONE Frame::NONE
@@ -1326,66 +1009,3 @@ pub enum Alignment {
Center, Center,
Right, 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");
}
_ => {}
}
}
}
+49 -199
View File
@@ -1,13 +1,11 @@
use crate::DEFAULT_PADDING;
use crate::bar::exec_powershell;
use crate::render::Grouping; use crate::render::Grouping;
use crate::widgets::widget::WidgetConfig; use crate::widgets::widget::WidgetConfig;
use crate::DEFAULT_PADDING;
use eframe::egui::Pos2; use eframe::egui::Pos2;
use eframe::egui::TextBuffer; use eframe::egui::TextBuffer;
use eframe::egui::Vec2; use eframe::egui::Vec2;
use komorebi_client::PathExt; use komorebi_client::KomorebiTheme;
use komorebi_client::Rect; use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
@@ -15,10 +13,9 @@ use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// The `komorebi.bar.json` configuration file reference for `v0.1.40` /// The `komorebi.bar.json` configuration file reference for `v0.1.36`
pub struct KomobarConfig { pub struct KomobarConfig {
/// Bar height /// Bar height (default: 50)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50)))]
pub height: Option<f32>, pub height: Option<f32>,
/// Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or /// 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 /// vertical definition which can each take a single value for a symmetric padding or two
@@ -76,31 +73,23 @@ pub struct KomobarConfig {
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html) /// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
pub frame: Option<FrameConfig>, pub frame: Option<FrameConfig>,
/// The monitor index or the full monitor options /// The monitor index or the full monitor options
#[cfg_attr(feature = "schemars", schemars(extend("default" = MonitorConfigOrIndex::Index(0))))] pub monitor: MonitorConfigOrIndex,
pub monitor: Option<MonitorConfigOrIndex>,
/// Font family /// Font family
pub font_family: Option<String>, pub font_family: Option<String>,
/// Font size /// Font size (default: 12.5)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12.5)))]
pub font_size: Option<f32>, pub font_size: Option<f32>,
/// Scale of the icons relative to the font_size [[1.0-2.0]] /// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 1.4)))]
pub icon_scale: Option<f32>, pub icon_scale: Option<f32>,
/// Max label width before text truncation /// Max label width before text truncation (default: 400.0)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 400.0)))]
pub max_label_width: Option<f32>, pub max_label_width: Option<f32>,
/// Theme /// Theme
pub theme: Option<KomobarTheme>, pub theme: Option<KomobarTheme>,
/// Alpha value for the color transparency [[0-255]] /// Alpha value for the color transparency [[0-255]] (default: 200)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 200)))]
pub transparency_alpha: Option<u8>, pub transparency_alpha: Option<u8>,
/// Spacing between widgets /// Spacing between widgets (default: 10.0)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10.0)))]
pub widget_spacing: Option<f32>, pub widget_spacing: Option<f32>,
/// Visual grouping for widgets /// Visual grouping for widgets
pub grouping: Option<Grouping>, pub grouping: Option<Grouping>,
/// Options for mouse interaction on the bar
pub mouse: Option<MouseConfig>,
/// Left side widgets (ordered left-to-right) /// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>, pub left_widgets: Vec<WidgetConfig>,
/// Center widgets (ordered left-to-right) /// Center widgets (ordered left-to-right)
@@ -126,9 +115,7 @@ impl KomobarConfig {
} }
if display { if display {
println!( println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
"\nYour bar configuration file contains some options that have been renamed or deprecated:\n"
);
for (canonical, aliases) in map { for (canonical, aliases) in map {
for alias in aliases { for alias in aliases {
if raw.contains(alias) { if raw.contains(alias) {
@@ -151,7 +138,6 @@ impl KomobarConfig {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Position configuration
pub struct PositionConfig { pub struct PositionConfig {
/// The desired starting position of the bar (0,0 = top left of the screen) /// The desired starting position of the bar (0,0 = top left of the screen)
#[serde(alias = "position")] #[serde(alias = "position")]
@@ -163,7 +149,6 @@ pub struct PositionConfig {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Frame configuration
pub struct FrameConfig { pub struct FrameConfig {
/// Margin inside the painted frame /// Margin inside the painted frame
pub inner_margin: Position, pub inner_margin: Position,
@@ -172,7 +157,6 @@ pub struct FrameConfig {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)] #[serde(untagged)]
/// Monitor configuration or monitor index
pub enum MonitorConfigOrIndex { pub enum MonitorConfigOrIndex {
/// The monitor index where you want the bar to show /// The monitor index where you want the bar to show
Index(usize), Index(usize),
@@ -182,7 +166,6 @@ pub enum MonitorConfigOrIndex {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Monitor configuration
pub struct MonitorConfig { pub struct MonitorConfig {
/// Komorebi monitor index of the monitor on which to render the bar /// Komorebi monitor index of the monitor on which to render the bar
pub index: usize, pub index: usize,
@@ -200,13 +183,9 @@ pub type Margin = SpacingKind;
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a // `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
// `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the // `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the
// individual values. // individual values.
/// Spacing kind
pub enum SpacingKind { pub enum SpacingKind {
/// Spacing applied to all sides
All(f32), All(f32),
/// Individual spacing applied to each side
Individual(IndividualSpacingConfig), Individual(IndividualSpacingConfig),
/// Grouped vertical and horizontal spacing
Grouped(GroupedSpacingConfig), Grouped(GroupedSpacingConfig),
} }
@@ -251,36 +230,25 @@ impl SpacingKind {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Grouped vertical and horizontal spacing
pub struct GroupedSpacingConfig { pub struct GroupedSpacingConfig {
/// Vertical grouped spacing
pub vertical: Option<GroupedSpacingOptions>, pub vertical: Option<GroupedSpacingOptions>,
/// Horizontal grouped spacing
pub horizontal: Option<GroupedSpacingOptions>, pub horizontal: Option<GroupedSpacingOptions>,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)] #[serde(untagged)]
/// Grouped spacing options
pub enum GroupedSpacingOptions { pub enum GroupedSpacingOptions {
/// Symmetrical grouped spacing
Symmetrical(f32), Symmetrical(f32),
/// Split grouped spacing
Split(f32, f32), Split(f32, f32),
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Individual spacing configuration
pub struct IndividualSpacingConfig { pub struct IndividualSpacingConfig {
/// Spacing for the top
pub top: f32, pub top: f32,
/// Spacing for the bottom
pub bottom: f32, pub bottom: f32,
/// Spacing for the left
pub left: f32, pub left: f32,
/// Spacing for the right
pub right: f32, pub right: f32,
} }
@@ -357,153 +325,6 @@ 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 { impl KomobarConfig {
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> { pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
let content = std::fs::read_to_string(path)?; let content = std::fs::read_to_string(path)?;
@@ -527,7 +348,6 @@ impl KomobarConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Position
pub struct Position { pub struct Position {
/// X coordinate /// X coordinate
pub x: f32, pub x: f32,
@@ -553,11 +373,45 @@ impl From<Position> for Pos2 {
} }
} }
pub use komorebi_themes::KomobarTheme; #[derive(Copy, 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>,
},
/// 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>,
},
}
impl From<KomorebiTheme> for KomobarTheme {
fn from(value: KomorebiTheme) -> Self {
match value {
KomorebiTheme::Catppuccin {
name, bar_accent, ..
} => Self::Catppuccin {
name,
accent: bar_accent,
},
KomorebiTheme::Base16 {
name, bar_accent, ..
} => Self::Base16 {
name,
accent: bar_accent,
},
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Label prefix
pub enum LabelPrefix { pub enum LabelPrefix {
/// Show no prefix /// Show no prefix
None, None,
@@ -571,7 +425,6 @@ pub enum LabelPrefix {
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Display format
pub enum DisplayFormat { pub enum DisplayFormat {
/// Show only icon /// Show only icon
Icon, Icon,
@@ -586,10 +439,9 @@ pub enum DisplayFormat {
} }
macro_rules! extend_enum { macro_rules! extend_enum {
($(#[$type_meta:meta])* $existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => { ($existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] #[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
$(#[$type_meta])*
pub enum $new_enum { pub enum $new_enum {
// Add new variants // Add new variants
$( $(
@@ -610,9 +462,7 @@ macro_rules! extend_enum {
}; };
} }
extend_enum!( extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
/// Workspaces display format
DisplayFormat, WorkspacesDisplayFormat, {
/// Show all icons only /// Show all icons only
AllIcons, AllIcons,
/// Show both all icons and text /// Show both all icons and text
+59 -48
View File
@@ -15,25 +15,26 @@ use eframe::egui::ViewportBuilder;
use font_loader::system_fonts; use font_loader::system_fonts;
use hotwatch::EventKind; use hotwatch::EventKind;
use hotwatch::Hotwatch; use hotwatch::Hotwatch;
use komorebi_client::PathExt; use image::RgbaImage;
use komorebi_client::SocketMessage; use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions; use komorebi_client::SubscribeOptions;
use komorebi_client::replace_env_in_path; use std::collections::HashMap;
use std::io::BufReader; use std::io::BufReader;
use std::io::Read; use std::io::Read;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::AtomicI32; use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::time::Duration; use std::time::Duration;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM; use windows::Win32::Foundation::LPARAM;
use windows::Win32::System::Threading::GetCurrentProcessId; use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::GetCurrentThreadId; use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext; use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows; use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId; use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
use windows_core::BOOL; use windows_core::BOOL;
@@ -46,8 +47,8 @@ pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
pub static BAR_HEIGHT: f32 = 50.0; pub static BAR_HEIGHT: f32 = 50.0;
pub static DEFAULT_PADDING: f32 = 10.0; pub static DEFAULT_PADDING: f32 = 10.0;
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0); pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0); LazyLock::new(|| Mutex::new(HashMap::new()));
#[derive(Parser)] #[derive(Parser)]
#[clap(author, about, version)] #[clap(author, about, version)]
@@ -60,7 +61,6 @@ struct Opts {
fonts: bool, fonts: bool,
/// Path to a JSON or YAML configuration file /// Path to a JSON or YAML configuration file
#[clap(short, long)] #[clap(short, long)]
#[clap(value_parser = replace_env_in_path)]
config: Option<PathBuf>, config: Option<PathBuf>,
/// Write an example komorebi.bar.json to disk /// Write an example komorebi.bar.json to disk
#[clap(long)] #[clap(long)]
@@ -103,7 +103,7 @@ fn process_hwnd() -> Option<isize> {
} }
pub enum KomorebiEvent { pub enum KomorebiEvent {
Notification(Box<komorebi_client::Notification>), Notification(komorebi_client::Notification),
Reconnect, Reconnect,
} }
@@ -114,8 +114,15 @@ fn main() -> color_eyre::Result<()> {
#[cfg(feature = "schemars")] #[cfg(feature = "schemars")]
if opts.schema { if opts.schema {
let bar_config = schemars::schema_for!(KomobarConfig); let settings = schemars::gen::SchemaSettings::default().with(|s| {
let schema = serde_json::to_string_pretty(&bar_config)?; s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
println!("{schema}"); println!("{schema}");
std::process::exit(0); std::process::exit(0);
@@ -130,17 +137,13 @@ fn main() -> color_eyre::Result<()> {
} }
if std::env::var("RUST_LIB_BACKTRACE").is_err() { if std::env::var("RUST_LIB_BACKTRACE").is_err() {
unsafe { std::env::set_var("RUST_LIB_BACKTRACE", "1");
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
} }
color_eyre::install()?; color_eyre::install()?;
if std::env::var("RUST_LOG").is_err() { if std::env::var("RUST_LOG").is_err() {
unsafe { std::env::set_var("RUST_LOG", "info");
std::env::set_var("RUST_LOG", "info");
}
} }
tracing::subscriber::set_global_default( tracing::subscriber::set_global_default(
@@ -152,14 +155,13 @@ fn main() -> color_eyre::Result<()> {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else( let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"), |_| dirs::home_dir().expect("there is no home directory"),
|home_path| { |home_path| {
let home = home_path.replace_env(); let home = PathBuf::from(&home_path);
assert!( if home.as_path().is_dir() {
home.is_dir(), home
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory" } else {
); panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
home
}, },
); );
@@ -168,7 +170,7 @@ fn main() -> color_eyre::Result<()> {
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?; std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
println!( println!(
"Example komorebi.bar.json file written to {}", "Example komorebi.bar.json file written to {}",
home_dir.display() home_dir.as_path().display()
); );
std::process::exit(0); std::process::exit(0);
@@ -176,11 +178,16 @@ fn main() -> color_eyre::Result<()> {
let default_config_path = home_dir.join("komorebi.bar.json"); let default_config_path = home_dir.join("komorebi.bar.json");
let config_path = opts.config.or_else(|| { let config_path = opts.config.map_or_else(
default_config_path || {
.is_file() if !default_config_path.is_file() {
.then_some(default_config_path.clone()) None
}); } else {
Some(default_config_path.clone())
}
},
Option::from,
);
let mut config = match config_path { let mut config = match config_path {
None => { None => {
@@ -190,14 +197,17 @@ fn main() -> color_eyre::Result<()> {
std::fs::write(&default_config_path, komorebi_bar_json)?; std::fs::write(&default_config_path, komorebi_bar_json)?;
tracing::info!( tracing::info!(
"created example configuration file: {}", "created example configuration file: {}",
default_config_path.display() default_config_path.as_path().display()
); );
KomobarConfig::read(&default_config_path)? KomobarConfig::read(&default_config_path)?
} }
Some(ref config) => { Some(ref config) => {
if !opts.aliases { if !opts.aliases {
tracing::info!("found configuration file: {}", config.display()); tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
} }
KomobarConfig::read(config)? KomobarConfig::read(config)?
@@ -216,30 +226,28 @@ fn main() -> color_eyre::Result<()> {
)?)?; )?)?;
let (usr_monitor_index, work_area_offset) = match &config.monitor { let (usr_monitor_index, work_area_offset) = match &config.monitor {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => { MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
(monitor_config.index, monitor_config.work_area_offset) (monitor_config.index, monitor_config.work_area_offset)
} }
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None), MonitorConfigOrIndex::Index(idx) => (*idx, None),
None => (0, None),
}; };
let monitor_index = state let monitor_index = state
.monitor_usr_idx_map .monitor_usr_idx_map
.get(&usr_monitor_index) .get(&usr_monitor_index)
.map_or(usr_monitor_index, |i| *i); .map_or(usr_monitor_index, |i| *i);
MONITOR_RIGHT.store( MONITOR_RIGHT.store(
state.monitors.elements()[monitor_index].size.right, state.monitors.elements()[monitor_index].size().right,
Ordering::SeqCst, Ordering::SeqCst,
); );
MONITOR_TOP.store( MONITOR_TOP.store(
state.monitors.elements()[monitor_index].size.top, state.monitors.elements()[monitor_index].size().top,
Ordering::SeqCst, Ordering::SeqCst,
); );
MONITOR_LEFT.store( MONITOR_LEFT.store(
state.monitors.elements()[monitor_index].size.left, state.monitors.elements()[monitor_index].size().left,
Ordering::SeqCst, Ordering::SeqCst,
); );
@@ -249,11 +257,11 @@ fn main() -> color_eyre::Result<()> {
None => { None => {
config.position = Some(PositionConfig { config.position = Some(PositionConfig {
start: Some(Position { start: Some(Position {
x: state.monitors.elements()[monitor_index].size.left as f32, x: state.monitors.elements()[monitor_index].size().left as f32,
y: state.monitors.elements()[monitor_index].size.top as f32, y: state.monitors.elements()[monitor_index].size().top as f32,
}), }),
end: Some(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, y: 50.0,
}), }),
}) })
@@ -261,14 +269,14 @@ fn main() -> color_eyre::Result<()> {
Some(ref mut position) => { Some(ref mut position) => {
if position.start.is_none() { if position.start.is_none() {
position.start = Some(Position { position.start = Some(Position {
x: state.monitors.elements()[monitor_index].size.left as f32, x: state.monitors.elements()[monitor_index].size().left as f32,
y: state.monitors.elements()[monitor_index].size.top as f32, y: state.monitors.elements()[monitor_index].size().top as f32,
}); });
} }
if position.end.is_none() { if position.end.is_none() {
position.end = Some(Position { 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, y: 50.0,
}) })
} }
@@ -299,7 +307,10 @@ fn main() -> color_eyre::Result<()> {
hotwatch.watch(config_path, move |event| match event.kind { hotwatch.watch(config_path, move |event| match event.kind {
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) { EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
Ok(updated) => { Ok(updated) => {
tracing::info!("configuration file updated: {}", config_path_cl.display()); tracing::info!(
"configuration file updated: {}",
config_path_cl.as_path().to_string_lossy()
);
if let Err(error) = tx_config.send(updated) { if let Err(error) = tx_config.send(updated) {
tracing::error!("could not send configuration update to gui: {error}") tracing::error!("could not send configuration update to gui: {error}")
@@ -353,7 +364,7 @@ fn main() -> color_eyre::Result<()> {
while komorebi_client::send_message( while komorebi_client::send_message(
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()), &SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
) )
.is_err() .is_err()
{ {
std::thread::sleep(Duration::from_secs(1)); std::thread::sleep(Duration::from_secs(1));
} }
@@ -376,7 +387,7 @@ fn main() -> color_eyre::Result<()> {
Ok(notification) => { Ok(notification) => {
tracing::debug!("received notification from komorebi"); tracing::debug!("received notification from komorebi");
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(Box::new(notification))) { if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
tracing::error!("could not send komorebi notification update to gui thread: {error}") tracing::error!("could not send komorebi notification update to gui thread: {error}")
} }
@@ -404,5 +415,5 @@ fn main() -> color_eyre::Result<()> {
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config))) Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
}), }),
) )
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string())) .map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
} }
+4 -30
View File
@@ -1,5 +1,3 @@
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::bar::Alignment; use crate::bar::Alignment;
use crate::config::KomobarConfig; use crate::config::KomobarConfig;
use crate::config::MonitorConfigOrIndex; use crate::config::MonitorConfigOrIndex;
@@ -13,38 +11,29 @@ use eframe::egui::Margin;
use eframe::egui::Shadow; use eframe::egui::Shadow;
use eframe::egui::TextStyle; use eframe::egui::TextStyle;
use eframe::egui::Ui; use eframe::egui::Ui;
use komorebi_client::Colour;
use komorebi_client::Rgb;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::Arc;
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0); static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "kind")] #[serde(tag = "kind")]
/// Grouping
pub enum Grouping { pub enum Grouping {
/// No grouping is applied /// No grouping is applied
#[cfg_attr(feature = "schemars", schemars(title = "None"))]
None, None,
/// Widgets are grouped as a whole /// Widgets are grouped as a whole
#[cfg_attr(feature = "schemars", schemars(title = "Bar"))]
Bar(GroupingConfig), Bar(GroupingConfig),
/// Widgets are grouped by alignment /// Widgets are grouped by alignment
#[cfg_attr(feature = "schemars", schemars(title = "Alignment"))]
Alignment(GroupingConfig), Alignment(GroupingConfig),
/// Widgets are grouped individually /// Widgets are grouped individually
#[cfg_attr(feature = "schemars", schemars(title = "Widget"))]
Widget(GroupingConfig), Widget(GroupingConfig),
} }
#[derive(Clone)] #[derive(Clone)]
/// Render configuration
pub struct RenderConfig { pub struct RenderConfig {
/// Komorebi monitor index of the monitor on which to render the bar /// Komorebi monitor index of the monitor on which to render the bar
pub monitor_idx: usize, pub monitor_idx: usize,
@@ -66,10 +55,6 @@ pub struct RenderConfig {
pub icon_font_id: FontId, pub icon_font_id: FontId,
/// Show all icons on the workspace section of the Komorebi widget /// Show all icons on the workspace section of the Komorebi widget
pub show_all_icons: bool, pub show_all_icons: bool,
/// Background color of the selected frame
pub auto_select_fill: Option<Color32>,
/// Text color of the selected frame
pub auto_select_text: Option<Color32>,
} }
pub trait RenderExt { pub trait RenderExt {
@@ -99,9 +84,8 @@ impl RenderExt for &KomobarConfig {
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0); icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
let monitor_idx = match &self.monitor { let monitor_idx = match &self.monitor {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => monitor_config.index, MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
Some(MonitorConfigOrIndex::Index(idx)) => *idx, MonitorConfigOrIndex::Index(idx) => *idx,
None => 0,
}; };
// check if any of the alignments have a komorebi widget with the workspace set to show all icons // check if any of the alignments have a komorebi widget with the workspace set to show all icons
@@ -124,10 +108,6 @@ impl RenderExt for &KomobarConfig {
text_font_id, text_font_id,
icon_font_id, icon_font_id,
show_all_icons, show_all_icons,
auto_select_fill: NonZeroU32::new(AUTO_SELECT_FILL_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
auto_select_text: NonZeroU32::new(AUTO_SELECT_TEXT_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
} }
} }
} }
@@ -153,8 +133,6 @@ impl RenderConfig {
text_font_id: FontId::default(), text_font_id: FontId::default(),
icon_font_id: FontId::default(), icon_font_id: FontId::default(),
show_all_icons: false, show_all_icons: false,
auto_select_fill: None,
auto_select_text: None,
} }
} }
@@ -363,7 +341,6 @@ impl RenderConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Grouping configuration
pub struct GroupingConfig { pub struct GroupingConfig {
/// Styles for the grouping /// Styles for the grouping
pub style: Option<GroupingStyle>, pub style: Option<GroupingStyle>,
@@ -375,9 +352,7 @@ pub struct GroupingConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Grouping Style
pub enum GroupingStyle { pub enum GroupingStyle {
/// Default
#[serde(alias = "CtByte")] #[serde(alias = "CtByte")]
Default, Default,
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3) /// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
@@ -399,9 +374,8 @@ pub enum GroupingStyle {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)] #[serde(untagged)]
/// Rounding configuration
pub enum RoundingConfig { pub enum RoundingConfig {
/// All 4 corners are the same /// All 4 corners are the same
Same(f32), Same(f32),
/// All 4 corners are custom. Order: NW, NE, SW, SE /// All 4 corners are custom. Order: NW, NE, SW, SE
Individual([f32; 4]), Individual([f32; 4]),
+4 -27
View File
@@ -10,29 +10,15 @@ use eframe::egui::Ui;
/// Same as SelectableLabel, but supports all content /// Same as SelectableLabel, but supports all content
pub struct SelectableFrame { pub struct SelectableFrame {
selected: bool, selected: bool,
selected_fill: Option<Color32>,
} }
impl SelectableFrame { impl SelectableFrame {
pub fn new(selected: bool) -> Self { pub fn new(selected: bool) -> Self {
Self { Self { selected }
selected,
selected_fill: None,
}
}
pub fn new_auto(selected: bool, selected_fill: Option<Color32>) -> Self {
Self {
selected,
selected_fill,
}
} }
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response { pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
let Self { let Self { selected } = self;
selected,
selected_fill,
} = self;
Frame::NONE Frame::NONE
.show(ui, |ui| { .show(ui, |ui| {
@@ -46,16 +32,7 @@ impl SelectableFrame {
); );
// since the stroke is drawn inside the frame, we always reserve space for it // since the stroke is drawn inside the frame, we always reserve space for it
if selected && response.hovered() { if response.hovered() || response.highlighted() || response.has_focus() {
let visuals = ui.style().interact_selectable(&response, selected);
Frame::NONE
.stroke(Stroke::new(1.0, visuals.bg_stroke.color))
.corner_radius(visuals.corner_radius)
.fill(selected_fill.unwrap_or(visuals.bg_fill))
.inner_margin(inner_margin)
.show(ui, add_contents);
} else if response.hovered() || response.highlighted() || response.has_focus() {
let visuals = ui.style().interact_selectable(&response, selected); let visuals = ui.style().interact_selectable(&response, selected);
Frame::NONE Frame::NONE
@@ -70,7 +47,7 @@ impl SelectableFrame {
Frame::NONE Frame::NONE
.stroke(Stroke::new(1.0, visuals.bg_fill)) .stroke(Stroke::new(1.0, visuals.bg_fill))
.corner_radius(visuals.corner_radius) .corner_radius(visuals.corner_radius)
.fill(selected_fill.unwrap_or(visuals.bg_fill)) .fill(visuals.bg_fill)
.inner_margin(inner_margin) .inner_margin(inner_margin)
.show(ui, add_contents); .show(ui, add_contents);
} else { } else {
-409
View File
@@ -1,409 +0,0 @@
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);
}
});
}
}
+32 -69
View File
@@ -2,36 +2,32 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame; use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::Label; use eframe::egui::Label;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use starship_battery::units::ratio::percent;
use starship_battery::Manager; use starship_battery::Manager;
use starship_battery::State; use starship_battery::State;
use starship_battery::units::ratio::percent;
use std::process::Command; use std::process::Command;
use std::time::Duration; use std::time::Duration;
use std::time::Instant; use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Battery widget configuration
pub struct BatteryConfig { pub struct BatteryConfig {
/// Enable the Battery widget /// Enable the Battery widget
pub enable: bool, pub enable: bool,
/// Hide the widget if the battery is at full charge /// Hide the widget if the battery is at full charge
pub hide_on_full_charge: Option<bool>, pub hide_on_full_charge: Option<bool>,
/// Data refresh interval in seconds /// Data refresh interval (default: 10 seconds)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is under this value [[1-100]]
pub auto_select_under: Option<u8>,
} }
impl From<BatteryConfig> for Battery { impl From<BatteryConfig> for Battery {
@@ -42,10 +38,9 @@ impl From<BatteryConfig> for Battery {
enable: value.enable, enable: value.enable,
hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false), hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false),
manager: Manager::new().unwrap(), manager: Manager::new().unwrap(),
last_state: None, last_state: String::new(),
data_refresh_interval, data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon), label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
auto_select_under: value.auto_select_under.map(|u| u.clamp(1, 100)),
state: BatteryState::Discharging, state: BatteryState::Discharging,
last_updated: Instant::now() last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval)) .checked_sub(Duration::from_secs(data_refresh_interval))
@@ -57,16 +52,6 @@ impl From<BatteryConfig> for Battery {
pub enum BatteryState { pub enum BatteryState {
Charging, Charging,
Discharging, Discharging,
High,
Medium,
Low,
Warning,
}
#[derive(Clone, Debug)]
struct BatteryOutput {
label: String,
selected: bool,
} }
pub struct Battery { pub struct Battery {
@@ -76,54 +61,38 @@ pub struct Battery {
pub state: BatteryState, pub state: BatteryState,
data_refresh_interval: u64, data_refresh_interval: u64,
label_prefix: LabelPrefix, label_prefix: LabelPrefix,
auto_select_under: Option<u8>, last_state: String,
last_state: Option<BatteryOutput>,
last_updated: Instant, last_updated: Instant,
} }
impl Battery { impl Battery {
fn output(&mut self) -> Option<BatteryOutput> { fn output(&mut self) -> String {
let mut output = self.last_state.clone(); let mut output = self.last_state.clone();
let now = Instant::now(); let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
output = None; output.clear();
if let Ok(mut batteries) = self.manager.batteries() if let Ok(mut batteries) = self.manager.batteries() {
&& let Some(Ok(first)) = batteries.nth(0) if let Some(Ok(first)) = batteries.nth(0) {
{ let percentage = first.state_of_charge().get::<percent>();
let percentage = first.state_of_charge().get::<percent>().round() as u8;
if percentage == 100 && self.hide_on_full_charge { if percentage == 100.0 && self.hide_on_full_charge {
output = None output = String::new()
} else { } else {
match first.state() { match first.state() {
State::Charging => self.state = BatteryState::Charging, State::Charging => self.state = BatteryState::Charging,
State::Discharging => { State::Discharging => self.state = BatteryState::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 = match self.label_prefix {
output = Some(BatteryOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => { LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage}%") format!("BAT: {percentage:.0}%")
} }
LabelPrefix::None | LabelPrefix::Icon => { LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
format!("{percentage}%") }
} }
},
selected,
})
} }
} }
@@ -139,50 +108,44 @@ impl BarWidget for Battery {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable { if self.enable {
let output = self.output(); let output = self.output();
if let Some(output) = output { if !output.is_empty() {
let emoji = match self.state { let emoji = match self.state {
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING, BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL, BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
BatteryState::High => egui_phosphor::regular::BATTERY_HIGH,
BatteryState::Medium => egui_phosphor::regular::BATTERY_MEDIUM,
BatteryState::Low => egui_phosphor::regular::BATTERY_LOW,
BatteryState::Warning => egui_phosphor::regular::BATTERY_WARNING,
}; };
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple( let mut layout_job = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(), LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
config.icon_font_id.clone(), config.icon_font_id.clone(),
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color), ctx.style().visuals.selection.stroke.color,
100.0, 100.0,
); );
layout_job.append( layout_job.append(
&output.label, &output,
10.0, 10.0,
TextFormat { TextFormat {
font_id: config.text_font_id.clone(), font_id: config.text_font_id.clone(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()), color: ctx.style().visuals.text_color(),
valign: Align::Center, valign: Align::Center,
..Default::default() ..Default::default()
}, },
); );
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| { config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new_auto(output.selected, auto_focus_fill) if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked() .clicked()
&& let Err(error) = Command::new("cmd.exe") {
if let Err(error) = Command::new("cmd.exe")
.args(["/C", "start", "ms-settings:batterysaver"]) .args(["/C", "start", "ms-settings:batterysaver"])
.spawn() .spawn()
{ {
eprintln!("{error}") eprintln!("{}", error)
}
} }
}); });
} }
+17 -36
View File
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame; use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::Label; use eframe::egui::Label;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::process::Command; use std::process::Command;
@@ -18,17 +18,13 @@ use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// CPU widget configuration
pub struct CpuConfig { pub struct CpuConfig {
/// Enable the Cpu widget /// Enable the Cpu widget
pub enable: bool, pub enable: bool,
/// Data refresh interval in seconds /// Data refresh interval (default: 10 seconds)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
} }
impl From<CpuConfig> for Cpu { impl From<CpuConfig> for Cpu {
@@ -42,7 +38,6 @@ impl From<CpuConfig> for Cpu {
), ),
data_refresh_interval, data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText), label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
last_updated: Instant::now() last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval)) .checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(), .unwrap(),
@@ -50,38 +45,26 @@ impl From<CpuConfig> for Cpu {
} }
} }
#[derive(Clone, Debug)]
struct CpuOutput {
label: String,
selected: bool,
}
pub struct Cpu { pub struct Cpu {
pub enable: bool, pub enable: bool,
system: System, system: System,
data_refresh_interval: u64, data_refresh_interval: u64,
label_prefix: LabelPrefix, label_prefix: LabelPrefix,
auto_select_over: Option<u8>,
last_updated: Instant, last_updated: Instant,
} }
impl Cpu { impl Cpu {
fn output(&mut self) -> CpuOutput { fn output(&mut self) -> String {
let now = Instant::now(); let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_cpu_usage(); self.system.refresh_cpu_usage();
self.last_updated = now; self.last_updated = now;
} }
let used = self.system.global_cpu_usage() as u8; let used = self.system.global_cpu_usage();
let selected = self.auto_select_over.is_some_and(|o| used >= o); match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
CpuOutput { LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {used}%"),
LabelPrefix::None | LabelPrefix::Icon => format!("{used}%"),
},
selected,
} }
} }
} }
@@ -90,9 +73,7 @@ impl BarWidget for Cpu {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable { if self.enable {
let output = self.output(); let output = self.output();
if !output.label.is_empty() { if !output.is_empty() {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple( let mut layout_job = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -101,31 +82,31 @@ impl BarWidget for Cpu {
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
config.icon_font_id.clone(), config.icon_font_id.clone(),
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color), ctx.style().visuals.selection.stroke.color,
100.0, 100.0,
); );
layout_job.append( layout_job.append(
&output.label, &output,
10.0, 10.0,
TextFormat { TextFormat {
font_id: config.text_font_id.clone(), font_id: config.text_font_id.clone(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()), color: ctx.style().visuals.text_color(),
valign: Align::Center, valign: Align::Center,
..Default::default() ..Default::default()
}, },
); );
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| { config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new_auto(output.selected, auto_focus_fill) if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked() .clicked()
&& let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{ {
eprintln!("{error}") if let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
} }
}); });
} }
+3 -8
View File
@@ -4,16 +4,15 @@ use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use chrono::Local; use chrono::Local;
use chrono_tz::Tz; use chrono_tz::Tz;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::Label; use eframe::egui::Label;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::WidgetText; use eframe::egui::WidgetText;
use eframe::egui::text::LayoutJob;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use std::time::Instant; use std::time::Instant;
@@ -62,7 +61,6 @@ impl CustomModifiers {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Date widget configuration
pub struct DateConfig { pub struct DateConfig {
/// Enable the Date widget /// Enable the Date widget
pub enable: bool, pub enable: bool,
@@ -105,7 +103,6 @@ impl From<DateConfig> for Date {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Date widget format
pub enum DateFormat { pub enum DateFormat {
/// Month/Date/Year format (09/08/24) /// Month/Date/Year format (09/08/24)
MonthDateYear, MonthDateYear,
@@ -116,10 +113,8 @@ pub enum DateFormat {
/// Day Date Month Year format (8 September 2024) /// Day Date Month Year format (8 September 2024)
DayDateMonthYear, DayDateMonthYear,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html) /// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
Custom(String), Custom(String),
/// Custom format with modifiers /// Custom format with modifiers
#[cfg_attr(feature = "schemars", schemars(title = "CustomModifiers"))]
CustomModifiers(CustomModifiers), CustomModifiers(CustomModifiers),
} }
@@ -171,7 +166,7 @@ impl Date {
.to_string() .to_string()
.trim() .trim()
.to_string(), .to_string(),
Err(_) => format!("Invalid timezone: {timezone}"), Err(_) => format!("Invalid timezone: {}", timezone),
}, },
None => Local::now() None => Local::now()
.format(&self.format.fmt_string()) .format(&self.format.fmt_string())
@@ -230,7 +225,7 @@ impl BarWidget for Date {
if SelectableFrame::new(false) if SelectableFrame::new(false)
.show(ui, |ui| { .show(ui, |ui| {
ui.add( ui.add(
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone()))) Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false), .selectable(false),
) )
}) })
+8 -12
View File
@@ -1,17 +1,15 @@
use crate::config::LabelPrefix; use crate::config::LabelPrefix;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use color_eyre::eyre; use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::Label; use eframe::egui::Label;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::WidgetText; use eframe::egui::WidgetText;
use eframe::egui::text::LayoutJob;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use std::time::Instant; use std::time::Instant;
use windows::Win32::Globalization::LCIDToLocaleName; use windows::Win32::Globalization::LCIDToLocaleName;
@@ -21,16 +19,15 @@ use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow; use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId; use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
const ERROR_TEXT: &str = "Error"; const ERROR_TEXT: &str = "Error";
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Keyboard widget configuration
pub struct KeyboardConfig { pub struct KeyboardConfig {
/// Enable the Input widget /// Enable the Input widget
pub enable: bool, pub enable: bool,
/// Data refresh interval /// Data refresh interval (default: 1 second)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
@@ -38,7 +35,9 @@ pub struct KeyboardConfig {
impl From<KeyboardConfig> for Keyboard { impl From<KeyboardConfig> for Keyboard {
fn from(value: KeyboardConfig) -> Self { fn from(value: KeyboardConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10); let data_refresh_interval = value
.data_refresh_interval
.unwrap_or(DEFAULT_DATA_REFRESH_INTERVAL);
Self { Self {
enable: value.enable, enable: value.enable,
@@ -81,7 +80,7 @@ pub struct Keyboard {
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string. /// - `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 /// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
/// invalid UTF-16 characters during conversion. /// invalid UTF-16 characters during conversion.
fn get_active_keyboard_layout() -> eyre::Result<String, ()> { fn get_active_keyboard_layout() -> Result<String, ()> {
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) }; let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) }; let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
@@ -170,10 +169,7 @@ impl BarWidget for Keyboard {
); );
config.apply_on_widget(true, ui, |ui| { config.apply_on_widget(true, ui, |ui| {
ui.add( ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
.selectable(false),
)
}); });
} }
} }
File diff suppressed because it is too large Load Diff
+60 -68
View File
@@ -2,7 +2,7 @@ use crate::config::DisplayFormat;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame; use crate::selected_frame::SelectableFrame;
use crate::widgets::komorebi::KomorebiLayoutConfig; use crate::widgets::komorebi::KomorebiLayoutConfig;
use color_eyre::eyre; use eframe::egui::vec2;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::CornerRadius; use eframe::egui::CornerRadius;
use eframe::egui::FontId; use eframe::egui::FontId;
@@ -13,12 +13,11 @@ use eframe::egui::Stroke;
use eframe::egui::StrokeKind; use eframe::egui::StrokeKind;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::Vec2; use eframe::egui::Vec2;
use eframe::egui::vec2;
use komorebi_client::SocketMessage; use komorebi_client::SocketMessage;
use serde::de::Error;
use serde::Deserialize; use serde::Deserialize;
use serde::Deserializer; use serde::Deserializer;
use serde::Serialize; use serde::Serialize;
use serde::de::Error;
use serde_json::from_str; use serde_json::from_str;
use std::fmt::Display; use std::fmt::Display;
use std::fmt::Formatter; use std::fmt::Formatter;
@@ -26,30 +25,24 @@ use std::fmt::Formatter;
#[derive(Copy, Clone, Debug, Serialize, PartialEq)] #[derive(Copy, Clone, Debug, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)] #[serde(untagged)]
/// Komorebi layout kind
pub enum KomorebiLayout { pub enum KomorebiLayout {
/// Predefined layout
#[cfg_attr(feature = "schemars", schemars(title = "Default"))]
Default(komorebi_client::DefaultLayout), Default(komorebi_client::DefaultLayout),
/// Monocle mode
Monocle, Monocle,
/// Floating layer
Floating, Floating,
/// Paused
Paused, Paused,
/// Custom layout
Custom, Custom,
} }
impl<'de> Deserialize<'de> for KomorebiLayout { impl<'de> Deserialize<'de> for KomorebiLayout {
fn deserialize<D>(deserializer: D) -> eyre::Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let s: String = String::deserialize(deserializer)?; let s: String = String::deserialize(deserializer)?;
// Attempt to deserialize the string as a DefaultLayout // 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)); return Ok(KomorebiLayout::Default(default_layout));
} }
@@ -60,7 +53,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
"Floating" => Ok(KomorebiLayout::Floating), "Floating" => Ok(KomorebiLayout::Floating),
"Paused" => Ok(KomorebiLayout::Paused), "Paused" => Ok(KomorebiLayout::Paused),
"Custom" => Ok(KomorebiLayout::Custom), "Custom" => Ok(KomorebiLayout::Custom),
_ => Err(Error::custom(format!("Invalid layout: {s}"))), _ => Err(Error::custom(format!("Invalid layout: {}", s))),
} }
} }
} }
@@ -99,15 +92,16 @@ impl KomorebiLayout {
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) { fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
match self { match self {
KomorebiLayout::Default(option) => { KomorebiLayout::Default(option) => {
if let Some(ws_idx) = workspace_idx if let Some(ws_idx) = workspace_idx {
&& komorebi_client::send_message(&SocketMessage::WorkspaceLayout( if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
monitor_idx, monitor_idx,
ws_idx, ws_idx,
*option, *option,
)) ))
.is_err() .is_err()
{ {
tracing::error!("could not send message to komorebi: WorkspaceLayout"); tracing::error!("could not send message to komorebi: WorkspaceLayout");
}
} }
} }
KomorebiLayout::Monocle => { KomorebiLayout::Monocle => {
@@ -194,12 +188,6 @@ impl KomorebiLayout {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke); 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); 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::Monocle => {}
KomorebiLayout::Floating => { KomorebiLayout::Floating => {
@@ -263,7 +251,7 @@ impl KomorebiLayout {
let layout_frame = SelectableFrame::new(false) let layout_frame = SelectableFrame::new(false)
.show(ui, |ui| { .show(ui, |ui| {
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format { if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
self.show_icon(true, font_id.clone(), ctx, ui); self.show_icon(false, font_id.clone(), ctx, ui);
} }
if let DisplayFormat::Text | DisplayFormat::IconAndText = format { if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
@@ -276,53 +264,57 @@ impl KomorebiLayout {
show_options = self.on_click(&show_options, monitor_idx, workspace_idx); show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
} }
if show_options && let Some(workspace_idx) = workspace_idx { if show_options {
Frame::NONE.show(ui, |ui| { if let Some(workspace_idx) = workspace_idx {
ui.add( Frame::NONE.show(ui, |ui| {
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string()) ui.add(
.selectable(false), Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
); .selectable(false),
);
let mut layout_options = layout_config.options.clone().unwrap_or(vec![ let mut layout_options = layout_config.options.clone().unwrap_or(vec![
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP), KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns), KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows), KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack), KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
KomorebiLayout::Default( KomorebiLayout::Default(
komorebi_client::DefaultLayout::RightMainVerticalStack, komorebi_client::DefaultLayout::RightMainVerticalStack,
), ),
KomorebiLayout::Default(komorebi_client::DefaultLayout::HorizontalStack), KomorebiLayout::Default(
KomorebiLayout::Default( komorebi_client::DefaultLayout::HorizontalStack,
komorebi_client::DefaultLayout::UltrawideVerticalStack, ),
), KomorebiLayout::Default(
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid), komorebi_client::DefaultLayout::UltrawideVerticalStack,
//KomorebiLayout::Custom, ),
KomorebiLayout::Monocle, KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
KomorebiLayout::Floating, //KomorebiLayout::Custom,
KomorebiLayout::Paused, KomorebiLayout::Monocle,
]); KomorebiLayout::Floating,
KomorebiLayout::Paused,
]);
for layout_option in &mut layout_options { for layout_option in &mut layout_options {
let is_selected = self == layout_option; let is_selected = self == layout_option;
if SelectableFrame::new(is_selected) if SelectableFrame::new(is_selected)
.show(ui, |ui| { .show(ui, |ui| {
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui) layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
}) })
.on_hover_text(match layout_option { .on_hover_text(match layout_option {
KomorebiLayout::Default(layout) => layout.to_string(), KomorebiLayout::Default(layout) => layout.to_string(),
KomorebiLayout::Monocle => "Toggle monocle".to_string(), KomorebiLayout::Monocle => "Toggle monocle".to_string(),
KomorebiLayout::Floating => "Toggle tiling".to_string(), KomorebiLayout::Floating => "Toggle tiling".to_string(),
KomorebiLayout::Paused => "Toggle pause".to_string(), KomorebiLayout::Paused => "Toggle pause".to_string(),
KomorebiLayout::Custom => "Custom".to_string(), KomorebiLayout::Custom => "Custom".to_string(),
}) })
.clicked() .clicked()
{ {
layout_option.on_click_option(monitor_idx, Some(workspace_idx)); layout_option.on_click_option(monitor_idx, Some(workspace_idx));
show_options = false; show_options = false;
}; };
} }
}); });
}
} }
}); });
+21 -20
View File
@@ -1,15 +1,15 @@
use crate::MAX_LABEL_WIDTH;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame; use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi; use crate::ui::CustomUi;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::Label; use eframe::egui::Label;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::Vec2; use eframe::egui::Vec2;
use eframe::egui::text::LayoutJob;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
@@ -17,7 +17,6 @@ use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Media widget configuration
pub struct MediaConfig { pub struct MediaConfig {
/// Enable the Media widget /// Enable the Media widget
pub enable: bool, pub enable: bool,
@@ -41,34 +40,36 @@ impl Media {
enable, enable,
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync() session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
.unwrap() .unwrap()
.join() .get()
.unwrap(), .unwrap(),
} }
} }
pub fn toggle(&self) { pub fn toggle(&self) {
if let Ok(session) = self.session_manager.GetCurrentSession() if let Ok(session) = self.session_manager.GetCurrentSession() {
&& let Ok(op) = session.TryTogglePlayPauseAsync() if let Ok(op) = session.TryTogglePlayPauseAsync() {
{ op.get().unwrap_or_default();
op.join().unwrap_or_default(); }
} }
} }
fn output(&mut self) -> String { fn output(&mut self) -> String {
if let Ok(session) = self.session_manager.GetCurrentSession() if let Ok(session) = self.session_manager.GetCurrentSession() {
&& let Ok(operation) = session.TryGetMediaPropertiesAsync() if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
&& let Ok(properties) = operation.join() if let Ok(properties) = operation.get() {
&& let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
{ if artist.is_empty() {
if artist.is_empty() { return format!("{title}");
return format!("{title}"); }
}
if title.is_empty() { if title.is_empty() {
return format!("{artist}"); return format!("{artist}");
} }
return format!("{artist} - {title}"); return format!("{artist} - {title}");
}
}
}
} }
String::new() String::new()
+18 -38
View File
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame; use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::Label; use eframe::egui::Label;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::process::Command; use std::process::Command;
@@ -18,17 +18,13 @@ use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Memory widget configuration
pub struct MemoryConfig { pub struct MemoryConfig {
/// Enable the Memory widget /// Enable the Memory widget
pub enable: bool, pub enable: bool,
/// Data refresh interval in seconds /// Data refresh interval (default: 10 seconds)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
} }
impl From<MemoryConfig> for Memory { impl From<MemoryConfig> for Memory {
@@ -42,7 +38,6 @@ impl From<MemoryConfig> for Memory {
), ),
data_refresh_interval, data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText), label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
last_updated: Instant::now() last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval)) .checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(), .unwrap(),
@@ -50,23 +45,16 @@ impl From<MemoryConfig> for Memory {
} }
} }
#[derive(Clone, Debug)]
struct MemoryOutput {
label: String,
selected: bool,
}
pub struct Memory { pub struct Memory {
pub enable: bool, pub enable: bool,
system: System, system: System,
data_refresh_interval: u64, data_refresh_interval: u64,
label_prefix: LabelPrefix, label_prefix: LabelPrefix,
auto_select_over: Option<u8>,
last_updated: Instant, last_updated: Instant,
} }
impl Memory { impl Memory {
fn output(&mut self) -> MemoryOutput { fn output(&mut self) -> String {
let now = Instant::now(); let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_memory(); self.system.refresh_memory();
@@ -75,17 +63,11 @@ impl Memory {
let used = self.system.used_memory(); let used = self.system.used_memory();
let total = self.system.total_memory(); let total = self.system.total_memory();
let usage = ((used * 100) / total) as u8; match self.label_prefix {
let selected = self.auto_select_over.is_some_and(|o| usage >= o); LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {}%", (used * 100) / total)
MemoryOutput { }
label: match self.label_prefix { LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {usage}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{usage}%"),
},
selected,
} }
} }
} }
@@ -94,9 +76,7 @@ impl BarWidget for Memory {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable { if self.enable {
let output = self.output(); let output = self.output();
if !output.label.is_empty() { if !output.is_empty() {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple( let mut layout_job = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -105,31 +85,31 @@ impl BarWidget for Memory {
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
config.icon_font_id.clone(), config.icon_font_id.clone(),
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color), ctx.style().visuals.selection.stroke.color,
100.0, 100.0,
); );
layout_job.append( layout_job.append(
&output.label, &output,
10.0, 10.0,
TextFormat { TextFormat {
font_id: config.text_font_id.clone(), font_id: config.text_font_id.clone(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()), color: ctx.style().visuals.text_color(),
valign: Align::Center, valign: Align::Center,
..Default::default() ..Default::default()
}, },
); );
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| { config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new_auto(output.selected, auto_focus_fill) if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked() .clicked()
&& let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{ {
eprintln!("{error}") if let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
} }
}); });
} }
-160
View File
@@ -1,15 +1,3 @@
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 battery;
pub mod cpu; pub mod cpu;
pub mod date; pub mod date;
@@ -23,151 +11,3 @@ pub mod storage;
pub mod time; pub mod time;
pub mod update; pub mod update;
pub mod widget; 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)
}
}
+138 -375
View File
@@ -1,100 +1,62 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix; use crate::config::LabelPrefix;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame; use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Color32;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::Label; use eframe::egui::Label;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use num_derive::FromPrimitive; use num_derive::FromPrimitive;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::fmt; use std::fmt;
use std::process::Command; 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::Duration;
use std::time::Instant; use std::time::Instant;
use sysinfo::Networks; use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network widget configuration
pub struct NetworkConfig { pub struct NetworkConfig {
/// Enable the Network widget /// Enable the Network widget
pub enable: bool, pub enable: bool,
/// Show total received and transmitted activity /// Show total data transmitted
#[serde(alias = "show_total_data_transmitted")] pub show_total_data_transmitted: bool,
pub show_total_activity: bool, /// Show network activity
/// Show received and transmitted activity pub show_network_activity: bool,
#[serde(alias = "show_network_activity")]
pub show_activity: bool,
/// Show default interface /// Show default interface
pub show_default_interface: Option<bool>, pub show_default_interface: Option<bool>,
/// Characters to reserve for received and transmitted activity /// Characters to reserve for network activity data
#[serde(alias = "network_activity_fill_characters")] pub network_activity_fill_characters: Option<usize>,
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>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
/// Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))
pub auto_select: Option<NetworkSelectConfig>,
}
#[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>,
/// Select the total transmitted data when it's over this value
pub total_transmitted_over: Option<u64>,
/// Select the received data when it's over this value
pub received_over: Option<u64>,
/// Select the transmitted data when it's over this value
pub transmitted_over: Option<u64>,
} }
impl From<NetworkConfig> for Network { impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self { fn from(value: NetworkConfig) -> Self {
let default_refresh_interval = 10; let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
let data_refresh_interval = value
.data_refresh_interval
.unwrap_or(default_refresh_interval);
Self { Self {
enable: value.enable, enable: value.enable,
show_total_activity: value.show_total_activity, show_total_activity: value.show_total_data_transmitted,
show_activity: value.show_activity, show_activity: value.show_network_activity,
show_default_interface: value.show_default_interface.unwrap_or(true), show_default_interface: value.show_default_interface.unwrap_or(true),
networks_network_activity: Arc::new(Mutex::new(Networks::new_with_refreshed_list())), networks_network_activity: Networks::new_with_refreshed_list(),
default_interface: Arc::new(Mutex::new(String::new())), default_interface: String::new(),
interface_generation: Arc::new(AtomicU64::new(0)),
default_refresh_interval,
data_refresh_interval, data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon), label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
auto_select: value.auto_select, network_activity_fill_characters: value
activity_left_padding: value.activity_left_padding.unwrap_or_default(), .network_activity_fill_characters
last_update_request_default_interface: Instant::now() .unwrap_or_default(),
.checked_sub(Duration::from_secs(default_refresh_interval)) last_state_total_activity: vec![],
last_state_activity: vec![],
last_updated_network_activity: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(), .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)),
} }
} }
} }
@@ -104,257 +66,177 @@ pub struct Network {
pub show_total_activity: bool, pub show_total_activity: bool,
pub show_activity: bool, pub show_activity: bool,
pub show_default_interface: bool, pub show_default_interface: bool,
networks_network_activity: Arc<Mutex<Networks>>, networks_network_activity: Networks,
default_refresh_interval: u64,
data_refresh_interval: u64, data_refresh_interval: u64,
label_prefix: LabelPrefix, label_prefix: LabelPrefix,
auto_select: Option<NetworkSelectConfig>, default_interface: String,
default_interface: Arc<Mutex<String>>, last_state_total_activity: Vec<NetworkReading>,
interface_generation: Arc<AtomicU64>, last_state_activity: Vec<NetworkReading>,
last_update_request_default_interface: Instant, last_updated_network_activity: Instant,
activity_generation: Arc<AtomicU64>, network_activity_fill_characters: usize,
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 { impl Network {
fn update_default_interface_async(&mut self) { fn default_interface(&mut self) {
let gen_ = self.interface_generation.fetch_add(1, Ordering::SeqCst) + 1; if let Ok(interface) = netdev::get_default_interface() {
let gen_arc = Arc::clone(&self.interface_generation); if let Some(friendly_name) = &interface.friendly_name {
let iface_arc = Arc::clone(&self.default_interface); self.default_interface.clone_from(friendly_name);
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 default_interface(&mut self) -> String { fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let current = self.default_interface.lock().unwrap().clone(); let mut activity = self.last_state_activity.clone();
let mut total_activity = self.last_state_total_activity.clone();
let now = Instant::now(); let now = Instant::now();
if now.duration_since(self.last_update_request_default_interface) if now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.default_refresh_interval) > Duration::from_secs(self.data_refresh_interval)
{ {
self.last_update_request_default_interface = now; activity.clear();
self.update_default_interface_async(); total_activity.clear();
}
current 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_network_activity_async(&mut self) { self.networks_network_activity.refresh(true);
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);
thread::spawn(move || { for (interface_name, data) in &self.networks_network_activity {
let mut activity = Vec::new(); if friendly_name.eq(interface_name) {
let mut total_activity = Vec::new(); if self.show_activity {
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
Self::to_pretty_bytes(
data.received(),
self.data_refresh_interval,
),
Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
),
));
}
if let Ok(interface) = netdev::get_default_interface() if self.show_total_activity {
&& let Some(friendly_name) = &interface.friendly_name total_activity.push(NetworkReading::new(
&& let Ok(mut networks) = networks_network_activity_arc.lock() NetworkReadingFormat::Total,
{ Self::to_pretty_bytes(data.total_received(), 1),
networks.refresh(true); 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);
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),
));
} }
} }
} }
} }
// Only update if this is the latest request self.last_state_activity.clone_from(&activity);
if gen_ == gen_arc.load(Ordering::SeqCst) { self.last_state_total_activity.clone_from(&total_activity);
if let Ok(mut act) = activity_arc.lock() { self.last_updated_network_activity = now;
*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) (activity, total_activity)
} }
fn reading_to_labels( fn reading_to_label(
&self, &self,
select_received: bool,
select_transmitted: bool,
ctx: &Context, ctx: &Context,
reading: &NetworkReading, reading: NetworkReading,
config: RenderConfig, config: RenderConfig,
) -> (Label, Label) { ) -> Label {
let (text_down, text_up) = match self.label_prefix { let (text_down, text_up) = match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => match reading.format { LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => ( NetworkReadingFormat::Speed => (
format!( format!(
"{: >width$}/s ", "{: >width$}/s ",
reading.received.pretty, reading.received_text,
width = self.activity_left_padding width = self.network_activity_fill_characters
), ),
format!( format!(
"{: >width$}/s", "{: >width$}/s",
reading.transmitted.pretty, reading.transmitted_text,
width = self.activity_left_padding width = self.network_activity_fill_characters
), ),
), ),
NetworkReadingFormat::Total => ( NetworkReadingFormat::Total => (
format!("{} ", reading.received.pretty), format!("{} ", reading.received_text),
reading.transmitted.pretty.clone(), reading.transmitted_text,
), ),
}, },
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format { LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
NetworkReadingFormat::Speed => ( NetworkReadingFormat::Speed => (
format!( format!(
"DOWN: {: >width$}/s ", "DOWN: {: >width$}/s ",
reading.received.pretty, reading.received_text,
width = self.activity_left_padding width = self.network_activity_fill_characters
), ),
format!( format!(
"UP: {: >width$}/s", "UP: {: >width$}/s",
reading.transmitted.pretty, reading.transmitted_text,
width = self.activity_left_padding width = self.network_activity_fill_characters
), ),
), ),
NetworkReadingFormat::Total => ( NetworkReadingFormat::Total => (
format!("\u{2211}DOWN: {}/s ", reading.received.pretty), format!("\u{2211}DOWN: {}/s ", reading.received_text),
format!("\u{2211}UP: {}/s", reading.transmitted.pretty), format!("\u{2211}UP: {}/s", reading.transmitted_text),
), ),
}, },
}; };
let auto_text_color_received = config.auto_select_text.filter(|_| select_received); let icon_format = TextFormat::simple(
let auto_text_color_transmitted = config.auto_select_text.filter(|_| select_transmitted); config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
);
let text_format = TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
};
// icon // icon
let mut layout_job_down = LayoutJob::simple( let mut layout_job = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
if select_received { egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
egui_phosphor::regular::ARROW_FAT_LINES_DOWN.to_string()
} else {
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
}
} }
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
config.icon_font_id.clone(), icon_format.font_id.clone(),
auto_text_color_received.unwrap_or(ctx.style().visuals.selection.stroke.color), icon_format.color,
100.0, 100.0,
); );
// text // text
layout_job_down.append( layout_job.append(
&text_down, &text_down,
ctx.style().spacing.item_spacing.x, ctx.style().spacing.item_spacing.x,
TextFormat { text_format.clone(),
font_id: config.text_font_id.clone(),
color: auto_text_color_received.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
); );
// icon // icon
let mut layout_job_up = LayoutJob::simple( layout_job.append(
match self.label_prefix { &match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
if select_transmitted { egui_phosphor::regular::ARROW_FAT_UP.to_string()
egui_phosphor::regular::ARROW_FAT_LINES_UP.to_string()
} else {
egui_phosphor::regular::ARROW_FAT_UP.to_string()
}
} }
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
config.icon_font_id.clone(), 0.0,
auto_text_color_transmitted.unwrap_or(ctx.style().visuals.selection.stroke.color), icon_format.clone(),
100.0,
); );
// text // text
layout_job_up.append( layout_job.append(
&text_up, &text_up,
ctx.style().spacing.item_spacing.x, ctx.style().spacing.item_spacing.x,
TextFormat { text_format.clone(),
font_id: config.text_font_id.clone(),
color: auto_text_color_transmitted.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
); );
( Label::new(layout_job).selectable(false)
Label::new(layout_job_down).selectable(false),
Label::new(layout_job_up).selectable(false),
)
} }
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> (u64, String) { fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32; let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32; let mut magnitude = input.log(1024f32) as u32;
@@ -366,29 +248,10 @@ impl Network {
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude); let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32; let result = input / ((1u64) << (magnitude * 10)) as f32;
( match base {
input as u64, Some(DataUnit::B) => format!("{result:.1} B"),
match base { Some(unit) => format!("{result:.1} {unit}iB"),
Some(DataUnit::B) => format!("{result:.1} B"), None => String::from("Unknown data unit"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
},
)
}
fn show_frame<R>(
&self,
selected: bool,
auto_focus_fill: Option<Color32>,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) {
if SelectableFrame::new_auto(selected, auto_focus_fill)
.show(ui, add_contents)
.clicked()
&& let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
{
eprintln!("{error}");
} }
} }
} }
@@ -396,8 +259,6 @@ impl Network {
impl BarWidget for Network { impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable { if self.enable {
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
// widget spacing: make sure to use the same config to call the apply_on_widget function // widget spacing: make sure to use the same config to call the apply_on_widget function
let mut render_config = config.clone(); let mut render_config = config.clone();
@@ -405,111 +266,26 @@ impl BarWidget for Network {
let (activity, total_activity) = self.network_activity(); let (activity, total_activity) = self.network_activity();
if self.show_total_activity { if self.show_total_activity {
for reading in &total_activity { for reading in total_activity {
render_config.apply_on_widget(false, ui, |ui| { render_config.apply_on_widget(true, ui, |ui| {
let select_received = self.auto_select.is_some_and(|f| { ui.add(self.reading_to_label(ctx, reading, config.clone()));
f.total_received_over
.is_some_and(|o| reading.received.value > o)
});
let select_transmitted = self.auto_select.is_some_and(|f| {
f.total_transmitted_over
.is_some_and(|o| reading.transmitted.value > o)
});
let labels = self.reading_to_labels(
select_received,
select_transmitted,
ctx,
reading,
config.clone(),
);
if is_reversed {
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
} else {
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
}
}); });
} }
} }
if self.show_activity { if self.show_activity {
for reading in &activity { for reading in activity {
render_config.apply_on_widget(false, ui, |ui| { render_config.apply_on_widget(true, ui, |ui| {
let select_received = self.auto_select.is_some_and(|f| { ui.add(self.reading_to_label(ctx, reading, config.clone()));
f.received_over.is_some_and(|o| reading.received.value > o)
});
let select_transmitted = self.auto_select.is_some_and(|f| {
f.transmitted_over
.is_some_and(|o| reading.transmitted.value > o)
});
let labels = self.reading_to_labels(
select_received,
select_transmitted,
ctx,
reading,
config.clone(),
);
if is_reversed {
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
} else {
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
}
}); });
} }
} }
} }
if self.show_default_interface { if self.show_default_interface {
let mut self_default_interface = self.default_interface(); self.default_interface();
if !self_default_interface.is_empty() { if !self.default_interface.is_empty() {
let mut layout_job = LayoutJob::simple( let mut layout_job = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -523,11 +299,11 @@ impl BarWidget for Network {
); );
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix { 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( layout_job.append(
&self_default_interface, &self.default_interface,
10.0, 10.0,
TextFormat { TextFormat {
font_id: config.text_font_id.clone(), font_id: config.text_font_id.clone(),
@@ -538,9 +314,15 @@ impl BarWidget for Network {
); );
render_config.apply_on_widget(false, ui, |ui| { render_config.apply_on_widget(false, ui, |ui| {
self.show_frame(false, None, ui, |ui| { if SelectableFrame::new(false)
ui.add(Label::new(layout_job).selectable(false)) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
}); .clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
{
eprintln!("{}", error)
}
}
}); });
} }
} }
@@ -557,38 +339,19 @@ enum NetworkReadingFormat {
Total = 1, Total = 1,
} }
#[derive(Clone)]
struct ReadingValue {
value: u64,
pretty: String,
}
impl From<(u64, String)> for ReadingValue {
fn from(value: (u64, String)) -> Self {
Self {
value: value.0,
pretty: value.1,
}
}
}
#[derive(Clone)] #[derive(Clone)]
struct NetworkReading { struct NetworkReading {
format: NetworkReadingFormat, pub format: NetworkReadingFormat,
received: ReadingValue, pub received_text: String,
transmitted: ReadingValue, pub transmitted_text: String,
} }
impl NetworkReading { impl NetworkReading {
fn new( pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
format: NetworkReadingFormat, NetworkReading {
received: ReadingValue,
transmitted: ReadingValue,
) -> Self {
Self {
format, format,
received, received_text: received,
transmitted, transmitted_text: transmitted,
} }
} }
} }
@@ -608,6 +371,6 @@ enum DataUnit {
impl fmt::Display for DataUnit { impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{self:?}") write!(f, "{:?}", self)
} }
} }
+22 -72
View File
@@ -1,14 +1,13 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix; use crate::config::LabelPrefix;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame; use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::Label; use eframe::egui::Label;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::process::Command; use std::process::Command;
@@ -18,25 +17,13 @@ use sysinfo::Disks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Storage widget configuration
pub struct StorageConfig { pub struct StorageConfig {
/// Enable the Storage widget /// Enable the Storage widget
pub enable: bool, pub enable: bool,
/// Data refresh interval in seconds /// Data refresh interval (default: 10 seconds)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, 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]]
pub auto_hide_under: Option<u8>,
} }
impl From<StorageConfig> for Storage { impl From<StorageConfig> for Storage {
@@ -46,34 +33,21 @@ impl From<StorageConfig> for Storage {
disks: Disks::new_with_refreshed_list(), disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10), data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText), 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(), last_updated: Instant::now(),
} }
} }
} }
struct StorageDisk {
label: String,
selected: bool,
}
pub struct Storage { pub struct Storage {
pub enable: bool, pub enable: bool,
disks: Disks, disks: Disks,
data_refresh_interval: u64, data_refresh_interval: u64,
label_prefix: LabelPrefix, 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, last_updated: Instant,
} }
impl Storage { impl Storage {
fn output(&mut self) -> Vec<StorageDisk> { fn output(&mut self) -> Vec<String> {
let now = Instant::now(); let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.disks.refresh(true); self.disks.refresh(true);
@@ -83,36 +57,21 @@ impl Storage {
let mut disks = vec![]; let mut disks = vec![];
for disk in &self.disks { 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 mount = disk.mount_point();
let total = disk.total_space(); let total = disk.total_space();
let available = disk.available_space(); let available = disk.available_space();
let used = total - available; let used = total - available;
let percentage = ((used * 100) / total) as u8;
let hide = self.auto_hide_under.is_some_and(|u| percentage <= u); disks.push(match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
if !hide { format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
let selected = self.auto_select_over.is_some_and(|o| percentage >= o); }
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
disks.push(StorageDisk { })
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), percentage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
},
selected,
})
}
} }
disks.sort_by(|a, b| a.label.cmp(&b.label)); disks.sort();
disks.reverse();
disks disks
} }
@@ -121,16 +80,7 @@ impl Storage {
impl BarWidget for Storage { impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable { if self.enable {
let mut output = self.output(); for output in self.output() {
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
if is_reversed {
output.reverse();
}
for output in output {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple( let mut layout_job = LayoutJob::simple(
match self.label_prefix { match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => { LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -139,36 +89,36 @@ impl BarWidget for Storage {
LabelPrefix::None | LabelPrefix::Text => String::new(), LabelPrefix::None | LabelPrefix::Text => String::new(),
}, },
config.icon_font_id.clone(), config.icon_font_id.clone(),
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color), ctx.style().visuals.selection.stroke.color,
100.0, 100.0,
); );
layout_job.append( layout_job.append(
&output.label, &output,
10.0, 10.0,
TextFormat { TextFormat {
font_id: config.text_font_id.clone(), font_id: config.text_font_id.clone(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()), color: ctx.style().visuals.text_color(),
valign: Align::Center, valign: Align::Center,
..Default::default() ..Default::default()
}, },
); );
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| { config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new_auto(output.selected, auto_focus_fill) if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked() .clicked()
&& let Err(error) = Command::new("cmd.exe") {
if let Err(error) = Command::new("cmd.exe")
.args([ .args([
"/C", "/C",
"explorer.exe", "explorer.exe",
output.label.split(' ').collect::<Vec<&str>>()[0], output.split(' ').collect::<Vec<&str>>()[0],
]) ])
.spawn() .spawn()
{ {
eprintln!("{error}") eprintln!("{}", error)
}
} }
}); });
} }
+3 -6
View File
@@ -6,6 +6,7 @@ use crate::widgets::widget::BarWidget;
use chrono::Local; use chrono::Local;
use chrono::NaiveTime; use chrono::NaiveTime;
use chrono_tz::Tz; use chrono_tz::Tz;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::CornerRadius; use eframe::egui::CornerRadius;
@@ -15,7 +16,6 @@ use eframe::egui::Stroke;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::Vec2; use eframe::egui::Vec2;
use eframe::egui::text::LayoutJob;
use eframe::epaint::StrokeKind; use eframe::epaint::StrokeKind;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::Deserialize; use serde::Deserialize;
@@ -72,7 +72,6 @@ lazy_static! {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Time widget configuration
pub struct TimeConfig { pub struct TimeConfig {
/// Enable the Time widget /// Enable the Time widget
pub enable: bool, pub enable: bool,
@@ -93,7 +92,7 @@ pub struct TimeConfig {
///} ///}
/// ``` /// ```
pub timezone: Option<String>, pub timezone: Option<String>,
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00 /// Change the icon depending on the time. The default icon is used between 8:30 and 12:00. (default: false)
pub changing_icon: Option<bool>, pub changing_icon: Option<bool>,
} }
@@ -120,7 +119,6 @@ impl From<TimeConfig> for Time {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Time format
pub enum TimeFormat { pub enum TimeFormat {
/// Twelve-hour format (with seconds) /// Twelve-hour format (with seconds)
TwelveHour, TwelveHour,
@@ -135,7 +133,6 @@ pub enum TimeFormat {
/// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock) /// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
BinaryRectangle, BinaryRectangle,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html) /// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
Custom(String), Custom(String),
} }
@@ -212,7 +209,7 @@ impl Time {
Some(dt.time()), Some(dt.time()),
) )
} }
Err(_) => (format!("Invalid timezone: {timezone:?}"), None), Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
}, },
None => { None => {
let dt = Local::now(); let dt = Local::now();
+7 -7
View File
@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame; use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget; use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align; use eframe::egui::Align;
use eframe::egui::Context; use eframe::egui::Context;
use eframe::egui::Label; use eframe::egui::Label;
use eframe::egui::TextFormat; use eframe::egui::TextFormat;
use eframe::egui::Ui; use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::process::Command; use std::process::Command;
@@ -16,12 +16,10 @@ use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Update widget configuration
pub struct UpdateConfig { pub struct UpdateConfig {
/// Enable the Update widget /// Enable the Update widget
pub enable: bool, pub enable: bool,
/// Data refresh interval in hours /// Data refresh interval (default: 12 hours)
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12)))]
pub data_refresh_interval: Option<u64>, pub data_refresh_interval: Option<u64>,
/// Display label prefix /// Display label prefix
pub label_prefix: Option<LabelPrefix>, pub label_prefix: Option<LabelPrefix>,
@@ -142,14 +140,16 @@ impl BarWidget for Update {
if SelectableFrame::new(false) if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked() .clicked()
&& let Err(error) = Command::new("explorer.exe") {
if let Err(error) = Command::new("explorer.exe")
.args([format!( .args([format!(
"https://github.com/LGUG2Z/komorebi/releases/v{}", "https://github.com/LGUG2Z/komorebi/releases/v{}",
self.latest_version self.latest_version
)]) )])
.spawn() .spawn()
{ {
eprintln!("{error}") eprintln!("{}", error)
}
} }
}); });
} }
+1 -31
View File
@@ -1,6 +1,4 @@
use crate::render::RenderConfig; use crate::render::RenderConfig;
use crate::widgets::applications::Applications;
use crate::widgets::applications::ApplicationsConfig;
use crate::widgets::battery::Battery; use crate::widgets::battery::Battery;
use crate::widgets::battery::BatteryConfig; use crate::widgets::battery::BatteryConfig;
use crate::widgets::cpu::Cpu; use crate::widgets::cpu::Cpu;
@@ -34,50 +32,23 @@ pub trait BarWidget {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Widget configuration
pub enum WidgetConfig { 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), Battery(BatteryConfig),
/// CPU widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Cpu"))]
Cpu(CpuConfig), Cpu(CpuConfig),
/// Date widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Date"))]
Date(DateConfig), Date(DateConfig),
/// Keyboard widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Keyboard"))]
Keyboard(KeyboardConfig), Keyboard(KeyboardConfig),
/// Komorebi widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Komorebi"))]
Komorebi(KomorebiConfig), Komorebi(KomorebiConfig),
/// Media widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Media"))]
Media(MediaConfig), Media(MediaConfig),
/// Memory widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Memory"))]
Memory(MemoryConfig), Memory(MemoryConfig),
/// Network widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Network"))]
Network(NetworkConfig), Network(NetworkConfig),
/// Storage widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
Storage(StorageConfig), Storage(StorageConfig),
/// Time widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
Time(TimeConfig), Time(TimeConfig),
/// Update widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Update"))]
Update(UpdateConfig), Update(UpdateConfig),
} }
impl WidgetConfig { impl WidgetConfig {
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> { pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
match self { match self {
WidgetConfig::Applications(config) => Box::new(Applications::from(config)),
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)), WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)), WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())), WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
@@ -94,7 +65,6 @@ impl WidgetConfig {
pub fn enabled(&self) -> bool { pub fn enabled(&self) -> bool {
match self { match self {
WidgetConfig::Applications(config) => config.enable,
WidgetConfig::Battery(config) => config.enable, WidgetConfig::Battery(config) => config.enable,
WidgetConfig::Cpu(config) => config.enable, WidgetConfig::Cpu(config) => config.enable,
WidgetConfig::Date(config) => config.enable, WidgetConfig::Date(config) => config.enable,
@@ -102,7 +72,7 @@ impl WidgetConfig {
WidgetConfig::Komorebi(config) => { WidgetConfig::Komorebi(config) => {
config.workspaces.as_ref().is_some_and(|w| w.enable) config.workspaces.as_ref().is_some_and(|w| w.enable)
|| config.layout.as_ref().is_some_and(|w| w.enable) || config.layout.as_ref().is_some_and(|w| w.enable)
|| config.focused_container.as_ref().is_some_and(|w| w.enable) || config.focused_window.as_ref().is_some_and(|w| w.enable)
|| config || config
.configuration_switcher .configuration_switcher
.as_ref() .as_ref()
+4 -4
View File
@@ -1,16 +1,16 @@
[package] [package]
name = "komorebi-client" name = "komorebi-client"
version = "0.1.40" version = "0.1.36"
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
komorebi = { path = "../komorebi", default-features = false } komorebi = { path = "../komorebi" }
uds_windows = { workspace = true } uds_windows = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
[features] [features]
default = ["schemars"] default = ["schemars"]
schemars = ["komorebi/default"] schemars = ["komorebi/schemars"]
+27 -40
View File
@@ -1,44 +1,20 @@
#![warn(clippy::all)] #![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_errors_doc)]
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::animation::prefix::AnimationPrefix;
pub use komorebi::animation::PerAnimationPrefixConfig;
pub use komorebi::asc::ApplicationSpecificConfiguration; pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::border_manager::BorderInfo; pub use komorebi::border_manager::BorderInfo;
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::config_generation::ApplicationConfiguration; pub use komorebi::config_generation::ApplicationConfiguration;
pub use komorebi::config_generation::IdWithIdentifier; pub use komorebi::config_generation::IdWithIdentifier;
pub use komorebi::config_generation::IdWithIdentifierAndComment; pub use komorebi::config_generation::IdWithIdentifierAndComment;
pub use komorebi::config_generation::MatchingRule; pub use komorebi::config_generation::MatchingRule;
pub use komorebi::config_generation::MatchingStrategy; pub use komorebi::config_generation::MatchingStrategy;
pub use komorebi::container::Container; pub use komorebi::container::Container;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::resolve_home_path;
pub use komorebi::core::AnimationStyle; pub use komorebi::core::AnimationStyle;
pub use komorebi::core::ApplicationIdentifier; pub use komorebi::core::ApplicationIdentifier;
pub use komorebi::core::Arrangement; pub use komorebi::core::Arrangement;
@@ -68,24 +44,38 @@ pub use komorebi::core::StackbarLabel;
pub use komorebi::core::StackbarMode; pub use komorebi::core::StackbarMode;
pub use komorebi::core::StateQuery; pub use komorebi::core::StateQuery;
pub use komorebi::core::WindowKind; 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::Monitor;
pub use komorebi::monitor_reconciliator::MonitorNotification; pub use komorebi::monitor_reconciliator::MonitorNotification;
pub use komorebi::ring::Ring; 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::win32_display_data;
pub use komorebi::window::Window; pub use komorebi::window::Window;
pub use komorebi::window_manager_event::WindowManagerEvent; pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace; pub use komorebi::workspace::Workspace;
pub use komorebi::workspace::WorkspaceGlobals; pub use komorebi::workspace::WorkspaceGlobals;
pub use komorebi::workspace::WorkspaceLayer; pub use komorebi::workspace::WorkspaceLayer;
pub use komorebi::AnimationsConfig;
pub use komorebi::AppSpecificConfigurationPath;
pub use komorebi::AspectRatio;
pub use komorebi::BorderColours;
pub use komorebi::CrossBoundaryBehaviour;
pub use komorebi::GlobalState;
pub use komorebi::KomorebiTheme;
pub use komorebi::MonitorConfig;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::PredefinedAspectRatio;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
pub use komorebi::WindowContainerBehaviour;
pub use komorebi::WindowsApi;
pub use komorebi::WorkspaceConfig;
use komorebi::DATA_DIR; use komorebi::DATA_DIR;
use std::borrow::Borrow;
use std::io::BufReader; use std::io::BufReader;
use std::io::Read; use std::io::Read;
use std::io::Write; use std::io::Write;
@@ -103,15 +93,12 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
stream.write_all(serde_json::to_string(message)?.as_bytes()) stream.write_all(serde_json::to_string(message)?.as_bytes())
} }
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()> pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
where
Q: Borrow<SocketMessage>,
{
let socket = DATA_DIR.join(KOMOREBI); let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?; let mut stream = UnixStream::connect(socket)?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?; stream.set_write_timeout(Some(Duration::from_secs(1)))?;
let msgs = messages.into_iter().fold(String::new(), |mut s, m| { let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
if let Ok(m_str) = serde_json::to_string(m.borrow()) { if let Ok(m_str) = serde_json::to_string(&m) {
s.push_str(&m_str); s.push_str(&m_str);
s.push('\n'); s.push('\n');
} }
+4 -4
View File
@@ -1,16 +1,16 @@
[package] [package]
name = "komorebi-gui" name = "komorebi-gui"
version = "0.1.40" version = "0.1.36"
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
komorebi-client = { path = "../komorebi-client", default-features = false } komorebi-client = { path = "../komorebi-client" }
eframe = { workspace = true } eframe = { workspace = true }
egui_extras = { workspace = true } egui_extras = { workspace = true }
random_word = { version = "0.5", features = ["en"] } random_word = { version = "0.5", features = ["en"] }
serde_json = { workspace = true } serde_json = { workspace = true }
windows-core = { workspace = true } windows-core = { workspace = true }
windows = { workspace = true } windows = { workspace = true }
+14 -50
View File
@@ -1,9 +1,9 @@
#![warn(clippy::all)] #![warn(clippy::all)]
use eframe::egui; use eframe::egui;
use eframe::egui::color_picker::Alpha;
use eframe::egui::Color32; use eframe::egui::Color32;
use eframe::egui::ViewportBuilder; use eframe::egui::ViewportBuilder;
use eframe::egui::color_picker::Alpha;
use komorebi_client::BorderStyle; use komorebi_client::BorderStyle;
use komorebi_client::Colour; use komorebi_client::Colour;
use komorebi_client::DefaultLayout; use komorebi_client::DefaultLayout;
@@ -41,9 +41,7 @@ struct BorderColours {
single: Color32, single: Color32,
stack: Color32, stack: Color32,
monocle: Color32, monocle: Color32,
floating: Color32,
unfocused: Color32, unfocused: Color32,
unfocused_locked: Color32,
} }
struct BorderConfig { struct BorderConfig {
@@ -78,8 +76,8 @@ impl From<&komorebi_client::Monitor> for MonitorConfig {
} }
Self { Self {
size: value.size, size: *value.size(),
work_area_offset: value.work_area_offset.unwrap_or_default(), work_area_offset: value.work_area_offset().unwrap_or_default(),
workspaces, workspaces,
} }
} }
@@ -95,22 +93,22 @@ struct WorkspaceConfig {
impl From<&komorebi_client::Workspace> for WorkspaceConfig { impl From<&komorebi_client::Workspace> for WorkspaceConfig {
fn from(value: &komorebi_client::Workspace) -> Self { fn from(value: &komorebi_client::Workspace) -> Self {
let layout = match value.layout { let layout = match value.layout() {
Layout::Default(layout) => layout, Layout::Default(layout) => *layout,
Layout::Custom(_) => DefaultLayout::BSP, Layout::Custom(_) => DefaultLayout::BSP,
}; };
let name = value let name = value
.name .name()
.to_owned() .to_owned()
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string()); .unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
Self { Self {
layout, layout,
name, name,
tile: value.tile, tile: *value.tile(),
workspace_padding: value.workspace_padding.unwrap_or(20), workspace_padding: value.workspace_padding().unwrap_or(20),
container_padding: value.container_padding.unwrap_or(20), container_padding: value.container_padding().unwrap_or(20),
} }
} }
} }
@@ -156,9 +154,7 @@ impl KomorebiGui {
single: colour32(global_state.border_colours.single), single: colour32(global_state.border_colours.single),
stack: colour32(global_state.border_colours.stack), stack: colour32(global_state.border_colours.stack),
monocle: colour32(global_state.border_colours.monocle), monocle: colour32(global_state.border_colours.monocle),
floating: colour32(global_state.border_colours.floating),
unfocused: colour32(global_state.border_colours.unfocused), unfocused: colour32(global_state.border_colours.unfocused),
unfocused_locked: colour32(global_state.border_colours.unfocused_locked),
}; };
let border_config = BorderConfig { let border_config = BorderConfig {
@@ -247,7 +243,7 @@ impl eframe::App for KomorebiGui {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ctx.set_pixels_per_point(2.0); ctx.set_pixels_per_point(2.0);
egui::ScrollArea::vertical().show(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| {
ui.set_width(ctx.content_rect().width()); ui.set_width(ctx.screen_rect().width());
ui.collapsing("Debugging", |ui| { ui.collapsing("Debugging", |ui| {
ui.collapsing("Window Rules", |ui| { ui.collapsing("Window Rules", |ui| {
let window = Window::from(self.debug_hwnd); let window = Window::from(self.debug_hwnd);
@@ -381,22 +377,6 @@ impl eframe::App for KomorebiGui {
} }
}); });
ui.collapsing("Floating", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.floating,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Floating,
self.border_config.border_colours.floating.r() as u32,
self.border_config.border_colours.floating.g() as u32,
self.border_config.border_colours.floating.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Unfocused", |ui| { ui.collapsing("Unfocused", |ui| {
if egui::color_picker::color_picker_color32( if egui::color_picker::color_picker_color32(
ui, ui,
@@ -411,22 +391,6 @@ impl eframe::App for KomorebiGui {
)) ))
.unwrap(); .unwrap();
} }
});
ui.collapsing("Unfocused Locked", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.unfocused_locked,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::UnfocusedLocked,
self.border_config.border_colours.unfocused_locked.r() as u32,
self.border_config.border_colours.unfocused_locked.g() as u32,
self.border_config.border_colours.unfocused_locked.b() as u32,
))
.unwrap();
}
}) })
}); });
@@ -437,7 +401,7 @@ impl eframe::App for KomorebiGui {
BorderStyle::Square, BorderStyle::Square,
] { ] {
if ui if ui
.add(egui::Button::selectable( .add(egui::SelectableLabel::new(
self.border_config.border_style == option, self.border_config.border_style == option,
option.to_string(), option.to_string(),
)) ))
@@ -494,7 +458,7 @@ impl eframe::App for KomorebiGui {
StackbarMode::Always, StackbarMode::Always,
] { ] {
if ui if ui
.add(egui::Button::selectable( .add(egui::SelectableLabel::new(
self.stackbar_config.mode == option, self.stackbar_config.mode == option,
option.to_string(), option.to_string(),
)) ))
@@ -513,7 +477,7 @@ impl eframe::App for KomorebiGui {
ui.collapsing("Label", |ui| { ui.collapsing("Label", |ui| {
for option in [StackbarLabel::Process, StackbarLabel::Title] { for option in [StackbarLabel::Process, StackbarLabel::Title] {
if ui if ui
.add(egui::Button::selectable( .add(egui::SelectableLabel::new(
self.stackbar_config.label == option, self.stackbar_config.label == option,
option.to_string(), option.to_string(),
)) ))
@@ -772,7 +736,7 @@ impl eframe::App for KomorebiGui {
DefaultLayout::Grid, DefaultLayout::Grid,
] { ] {
if ui if ui
.add(egui::Button::selectable( .add(egui::SelectableLabel::new(
workspace.layout == option, workspace.layout == option,
option.to_string(), option.to_string(),
)) ))
-26
View File
@@ -1,26 +0,0 @@
[package]
name = "komorebi-layouts"
version = "0.1.40"
edition = "2024"
[dependencies]
clap = { workspace = true }
color-eyre = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
strum = { workspace = true }
tracing = { workspace = true }
# Optional dependencies
schemars = { workspace = true, optional = true }
windows = { workspace = true, optional = true }
objc2-core-foundation = { version = "0.3", default-features = false, features = [
"std",
"CFCGTypes",
], optional = true }
[features]
schemars = ["dep:schemars"]
win32 = ["dep:windows"]
darwin = ["dep:objc2-core-foundation"]
File diff suppressed because it is too large Load Diff
-778
View File
@@ -1,778 +0,0 @@
use clap::ValueEnum;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
use super::OperationDirection;
use super::Rect;
use super::Sizing;
/// Maximum number of ratio values that can be specified for column_ratios and row_ratios
pub const MAX_RATIOS: usize = 5;
/// Minimum allowed ratio value (prevents zero-sized windows)
pub const MIN_RATIO: f32 = 0.1;
/// Maximum allowed ratio value (ensures space for remaining windows)
pub const MAX_RATIO: f32 = 0.9;
/// Default ratio value when none is specified
pub const DEFAULT_RATIO: f32 = 0.5;
/// Default secondary ratio value for UltrawideVerticalStack layout
pub const DEFAULT_SECONDARY_RATIO: f32 = 0.25;
#[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`
}
/// Helper to deserialize a variable-length array into a fixed [Option<f32>; MAX_RATIOS]
/// Ratios are truncated when their cumulative sum reaches or exceeds 1.0 to ensure
/// there's always remaining space for additional windows.
fn deserialize_ratios<'de, D>(
deserializer: D,
) -> Result<Option<[Option<f32>; MAX_RATIOS]>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<Vec<f32>> = Option::deserialize(deserializer)?;
Ok(opt.map(|vec| {
let mut arr = [None; MAX_RATIOS];
let mut cumulative_sum = 0.0_f32;
for (i, &val) in vec.iter().take(MAX_RATIOS).enumerate() {
let clamped_val = val.clamp(MIN_RATIO, MAX_RATIO);
// Only add this ratio if cumulative sum stays below 1.0
if cumulative_sum + clamped_val < 1.0 {
arr[i] = Some(clamped_val);
cumulative_sum += clamped_val;
} else {
// Stop adding ratios - cumulative sum would reach or exceed 1.0
tracing::debug!(
"Truncating ratios at index {} - cumulative sum {} + {} would reach/exceed 1.0",
i,
cumulative_sum,
clamped_val
);
break;
}
}
arr
}))
}
/// Helper to serialize [Option<f32>; MAX_RATIOS] as a compact array (without trailing nulls)
fn serialize_ratios<S>(
value: &Option<[Option<f32>; MAX_RATIOS]>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match value {
None => serializer.serialize_none(),
Some(arr) => {
// Find last non-None index
let last_idx = arr
.iter()
.rposition(|x| x.is_some())
.map(|i| i + 1)
.unwrap_or(0);
let vec: Vec<f32> = arr.iter().take(last_idx).filter_map(|&x| x).collect();
serializer.serialize_some(&vec)
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, 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>,
/// Column width ratios (up to MAX_RATIOS values between 0.1 and 0.9)
///
/// - Used by Columns layout: ratios for each column width
/// - Used by Grid layout: ratios for column widths
/// - Used by BSP, VerticalStack, RightMainVerticalStack: column_ratios[0] as primary split ratio
/// - Used by HorizontalStack: column_ratios[0] as primary split ratio (top area height)
/// - Used by UltrawideVerticalStack: column_ratios[0] as center ratio, column_ratios[1] as left ratio
///
/// Columns without a ratio share remaining space equally.
/// Example: `[0.3, 0.4, 0.3]` for 30%-40%-30% columns
#[serde(
default,
deserialize_with = "deserialize_ratios",
serialize_with = "serialize_ratios"
)]
pub column_ratios: Option<[Option<f32>; MAX_RATIOS]>,
/// Row height ratios (up to MAX_RATIOS values between 0.1 and 0.9)
///
/// - Used by Rows layout: ratios for each row height
/// - Used by Grid layout: ratios for row heights
///
/// Rows without a ratio share remaining space equally.
/// Example: `[0.5, 0.5]` for 50%-50% rows
#[serde(
default,
deserialize_with = "deserialize_ratios",
serialize_with = "serialize_ratios"
)]
pub row_ratios: Option<[Option<f32>; MAX_RATIOS]>,
}
#[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 {
Self::UltrawideVerticalStack | Self::RightMainVerticalStack => match len {
n if n > 1 => 1,
_ => 0,
},
Self::Scrolling => 0,
DefaultLayout::BSP
| DefaultLayout::Columns
| DefaultLayout::Rows
| DefaultLayout::VerticalStack
| DefaultLayout::HorizontalStack
| DefaultLayout::Grid => 0,
}
}
pub fn rightmost_index(&self, len: usize) -> usize {
match self {
DefaultLayout::BSP
| DefaultLayout::Columns
| DefaultLayout::Rows
| DefaultLayout::VerticalStack
| DefaultLayout::HorizontalStack
| DefaultLayout::Grid => len.saturating_sub(1),
DefaultLayout::UltrawideVerticalStack => match len {
2 => 0,
_ => len.saturating_sub(1),
},
DefaultLayout::RightMainVerticalStack => 0,
DefaultLayout::Scrolling => len.saturating_sub(1),
}
}
#[must_use]
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
pub fn resize(
&self,
unaltered: &Rect,
resize: &Option<Rect>,
edge: OperationDirection,
sizing: Sizing,
delta: i32,
) -> Option<Rect> {
if !matches!(
self,
Self::BSP
| Self::Columns
| Self::Rows
| Self::VerticalStack
| Self::RightMainVerticalStack
| Self::HorizontalStack
| Self::UltrawideVerticalStack
| Self::Scrolling
) {
return None;
};
let mut r = resize.unwrap_or_default();
let resize_delta = delta;
match edge {
OperationDirection::Left => match sizing {
Sizing::Increase => {
// Some final checks to make sure the user can't infinitely resize to
// the point of pushing other windows out of bounds
// Note: These checks cannot take into account the changes made to the
// edges of adjacent windows at operation time, so it is still possible
// to push windows out of bounds by maxing out an Increase Left on a
// Window with index 1, and then maxing out a Decrease Right on a Window
// with index 0. I don't think it's worth trying to defensively program
// against this; if people end up in this situation they are better off
// just hitting the retile command
let diff = ((r.left + -resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
r.left += -resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.left - -resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
r.left -= -resize_delta;
}
}
},
OperationDirection::Up => match sizing {
Sizing::Increase => {
let diff = ((r.top + resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
r.top += -resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.top - resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
r.top -= -resize_delta;
}
}
},
OperationDirection::Right => match sizing {
Sizing::Increase => {
let diff = ((r.right + resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
r.right += resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.right - resize_delta) as f32).abs();
if diff < unaltered.right as f32 {
r.right -= resize_delta;
}
}
},
OperationDirection::Down => match sizing {
Sizing::Increase => {
let diff = ((r.bottom + resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
r.bottom += resize_delta;
}
}
Sizing::Decrease => {
let diff = ((r.bottom - resize_delta) as f32).abs();
if diff < unaltered.bottom as f32 {
r.bottom -= resize_delta;
}
}
},
};
if r.eq(&Rect::default()) {
None
} else {
Option::from(r)
}
}
#[must_use]
pub const fn cycle_next(self) -> Self {
match self {
Self::BSP => Self::Columns,
Self::Columns => Self::Rows,
Self::Rows => Self::VerticalStack,
Self::VerticalStack => Self::HorizontalStack,
Self::HorizontalStack => Self::UltrawideVerticalStack,
Self::UltrawideVerticalStack => Self::Grid,
Self::Grid => Self::RightMainVerticalStack,
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,
Self::HorizontalStack => Self::VerticalStack,
Self::VerticalStack => Self::Rows,
Self::Rows => Self::Columns,
Self::Columns => Self::BSP,
Self::BSP => Self::RightMainVerticalStack,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// Helper to create LayoutOptions with column ratios
fn layout_options_with_column_ratios(ratios: &[f32]) -> LayoutOptions {
let mut arr = [None; MAX_RATIOS];
for (i, &r) in ratios.iter().take(MAX_RATIOS).enumerate() {
arr[i] = Some(r);
}
LayoutOptions {
scrolling: None,
grid: None,
column_ratios: Some(arr),
row_ratios: None,
}
}
// Helper to create LayoutOptions with row ratios
fn layout_options_with_row_ratios(ratios: &[f32]) -> LayoutOptions {
let mut arr = [None; MAX_RATIOS];
for (i, &r) in ratios.iter().take(MAX_RATIOS).enumerate() {
arr[i] = Some(r);
}
LayoutOptions {
scrolling: None,
grid: None,
column_ratios: None,
row_ratios: Some(arr),
}
}
// Helper to create LayoutOptions with both column and row ratios
fn layout_options_with_ratios(column_ratios: &[f32], row_ratios: &[f32]) -> LayoutOptions {
let mut col_arr = [None; MAX_RATIOS];
for (i, &r) in column_ratios.iter().take(MAX_RATIOS).enumerate() {
col_arr[i] = Some(r);
}
let mut row_arr = [None; MAX_RATIOS];
for (i, &r) in row_ratios.iter().take(MAX_RATIOS).enumerate() {
row_arr[i] = Some(r);
}
LayoutOptions {
scrolling: None,
grid: None,
column_ratios: Some(col_arr),
row_ratios: Some(row_arr),
}
}
mod deserialize_ratios_tests {
use super::*;
#[test]
fn test_deserialize_valid_ratios() {
let json = r#"{"column_ratios": [0.3, 0.4, 0.2]}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
let ratios = opts.column_ratios.unwrap();
assert_eq!(ratios[0], Some(0.3));
assert_eq!(ratios[1], Some(0.4));
assert_eq!(ratios[2], Some(0.2));
assert_eq!(ratios[3], None);
assert_eq!(ratios[4], None);
}
#[test]
fn test_deserialize_clamps_values_to_min() {
// Values below MIN_RATIO should be clamped
let json = r#"{"column_ratios": [0.05]}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
let ratios = opts.column_ratios.unwrap();
assert_eq!(ratios[0], Some(MIN_RATIO)); // Clamped to 0.1
}
#[test]
fn test_deserialize_clamps_values_to_max() {
// Values above MAX_RATIO should be clamped
let json = r#"{"column_ratios": [0.95]}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
let ratios = opts.column_ratios.unwrap();
// 0.9 is the max, so it should be clamped
assert!(ratios[0].unwrap() <= MAX_RATIO);
}
#[test]
fn test_deserialize_truncates_when_sum_exceeds_one() {
// Sum of ratios should not reach 1.0
// [0.5, 0.4] = 0.9, then 0.3 would make it 1.2, so it should be truncated
let json = r#"{"column_ratios": [0.5, 0.4, 0.3]}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
let ratios = opts.column_ratios.unwrap();
assert_eq!(ratios[0], Some(0.5));
assert_eq!(ratios[1], Some(0.4));
// Third ratio should be truncated because 0.5 + 0.4 + 0.3 >= 1.0
assert_eq!(ratios[2], None);
}
#[test]
fn test_deserialize_truncates_at_max_ratios() {
// More than MAX_RATIOS values should be truncated
let json = r#"{"column_ratios": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
let ratios = opts.column_ratios.unwrap();
// Only MAX_RATIOS (5) values should be stored
for i in 0..MAX_RATIOS {
assert_eq!(ratios[i], Some(0.1));
}
}
#[test]
fn test_deserialize_empty_array() {
let json = r#"{"column_ratios": []}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
let ratios = opts.column_ratios.unwrap();
for i in 0..MAX_RATIOS {
assert_eq!(ratios[i], None);
}
}
#[test]
fn test_deserialize_null() {
let json = r#"{"column_ratios": null}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
assert!(opts.column_ratios.is_none());
}
#[test]
fn test_deserialize_row_ratios() {
let json = r#"{"row_ratios": [0.3, 0.5]}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
let ratios = opts.row_ratios.unwrap();
assert_eq!(ratios[0], Some(0.3));
assert_eq!(ratios[1], Some(0.5));
assert_eq!(ratios[2], None);
}
}
mod serialize_ratios_tests {
use super::*;
#[test]
fn test_serialize_ratios_compact() {
let opts = layout_options_with_column_ratios(&[0.3, 0.4]);
let json = serde_json::to_string(&opts).unwrap();
// Should serialize ratios as compact array without trailing nulls in the ratios array
assert!(json.contains("0.3") && json.contains("0.4"));
}
#[test]
fn test_serialize_none_ratios() {
let opts = LayoutOptions {
scrolling: None,
grid: None,
column_ratios: None,
row_ratios: None,
};
let json = serde_json::to_string(&opts).unwrap();
// None values should serialize as null or be omitted
assert!(!json.contains("["));
}
#[test]
fn test_roundtrip_serialization() {
let original = layout_options_with_column_ratios(&[0.3, 0.4, 0.2]);
let json = serde_json::to_string(&original).unwrap();
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
assert_eq!(original.column_ratios, deserialized.column_ratios);
}
#[test]
fn test_serialize_row_ratios() {
let opts = layout_options_with_row_ratios(&[0.3, 0.5]);
let json = serde_json::to_string(&opts).unwrap();
assert!(json.contains("row_ratios"));
assert!(json.contains("0.3") && json.contains("0.5"));
}
#[test]
fn test_roundtrip_row_ratios() {
let original = layout_options_with_row_ratios(&[0.4, 0.3]);
let json = serde_json::to_string(&original).unwrap();
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
assert_eq!(original.row_ratios, deserialized.row_ratios);
assert!(original.column_ratios.is_none());
}
#[test]
fn test_roundtrip_both_ratios() {
let original = layout_options_with_ratios(&[0.3, 0.4], &[0.5, 0.3]);
let json = serde_json::to_string(&original).unwrap();
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
assert_eq!(original.column_ratios, deserialized.column_ratios);
assert_eq!(original.row_ratios, deserialized.row_ratios);
}
}
mod ratio_constants_tests {
use super::*;
#[test]
fn test_constants_valid_ranges() {
assert!(MIN_RATIO > 0.0);
assert!(MIN_RATIO < MAX_RATIO);
assert!(MAX_RATIO < 1.0);
assert!(DEFAULT_RATIO >= MIN_RATIO && DEFAULT_RATIO <= MAX_RATIO);
assert!(DEFAULT_SECONDARY_RATIO >= MIN_RATIO && DEFAULT_SECONDARY_RATIO <= MAX_RATIO);
assert!(MAX_RATIOS >= 1);
}
#[test]
fn test_default_ratio_is_half() {
assert_eq!(DEFAULT_RATIO, 0.5);
}
#[test]
fn test_max_ratios_is_five() {
assert_eq!(MAX_RATIOS, 5);
}
}
mod layout_options_tests {
use super::*;
#[test]
fn test_layout_options_default_values() {
let json = r#"{}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
assert!(opts.scrolling.is_none());
assert!(opts.grid.is_none());
assert!(opts.column_ratios.is_none());
assert!(opts.row_ratios.is_none());
}
#[test]
fn test_layout_options_with_all_fields() {
let json = r#"{
"scrolling": {"columns": 3},
"grid": {"rows": 2},
"column_ratios": [0.3, 0.4],
"row_ratios": [0.5]
}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
assert!(opts.scrolling.is_some());
assert_eq!(opts.scrolling.unwrap().columns, 3);
assert!(opts.grid.is_some());
assert_eq!(opts.grid.unwrap().rows, 2);
assert!(opts.column_ratios.is_some());
assert!(opts.row_ratios.is_some());
}
}
mod default_layout_tests {
use super::*;
#[test]
fn test_cycle_next_covers_all_layouts() {
let start = DefaultLayout::BSP;
let mut current = start;
let mut visited = vec![current];
loop {
current = current.cycle_next();
if current == start {
break;
}
assert!(
!visited.contains(&current),
"Cycle contains duplicate: {:?}",
current
);
visited.push(current);
}
// Should have visited all layouts
assert_eq!(visited.len(), 9); // 9 layouts total
}
#[test]
fn test_cycle_previous_is_inverse_of_next() {
// Note: cycle_previous has some inconsistencies in the current implementation
// This test documents the expected behavior for most layouts
let layouts_with_correct_inverse = [
DefaultLayout::Columns,
DefaultLayout::Rows,
DefaultLayout::VerticalStack,
DefaultLayout::HorizontalStack,
DefaultLayout::UltrawideVerticalStack,
DefaultLayout::Grid,
DefaultLayout::RightMainVerticalStack,
];
for layout in layouts_with_correct_inverse {
let next = layout.cycle_next();
assert_eq!(
next.cycle_previous(),
layout,
"cycle_previous should be inverse of cycle_next for {:?}",
layout
);
}
}
#[test]
fn test_leftmost_index_standard_layouts() {
assert_eq!(DefaultLayout::BSP.leftmost_index(5), 0);
assert_eq!(DefaultLayout::Columns.leftmost_index(5), 0);
assert_eq!(DefaultLayout::Rows.leftmost_index(5), 0);
assert_eq!(DefaultLayout::VerticalStack.leftmost_index(5), 0);
assert_eq!(DefaultLayout::HorizontalStack.leftmost_index(5), 0);
assert_eq!(DefaultLayout::Grid.leftmost_index(5), 0);
}
#[test]
fn test_leftmost_index_ultrawide() {
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(1), 0);
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(2), 1);
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(5), 1);
}
#[test]
fn test_leftmost_index_right_main() {
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(1), 0);
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(2), 1);
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(5), 1);
}
#[test]
fn test_rightmost_index_standard_layouts() {
assert_eq!(DefaultLayout::BSP.rightmost_index(5), 4);
assert_eq!(DefaultLayout::Columns.rightmost_index(5), 4);
assert_eq!(DefaultLayout::Rows.rightmost_index(5), 4);
assert_eq!(DefaultLayout::VerticalStack.rightmost_index(5), 4);
}
#[test]
fn test_rightmost_index_right_main() {
assert_eq!(DefaultLayout::RightMainVerticalStack.rightmost_index(1), 0);
assert_eq!(DefaultLayout::RightMainVerticalStack.rightmost_index(5), 0);
}
#[test]
fn test_rightmost_index_ultrawide() {
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(1), 0);
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(2), 0);
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(3), 2);
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(5), 4);
}
}
}
-30
View File
@@ -1,30 +0,0 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
//! Layout system for the komorebi window manager.
//!
//! This crate provides the core layout algorithms and types for arranging windows
//! in various configurations. It includes optional Windows-specific functionality
//! behind the `win32` feature flag.
pub mod arrangement;
#[cfg(feature = "win32")]
pub mod custom_layout;
pub mod cycle_direction;
pub mod default_layout;
pub mod direction;
pub mod layout;
pub mod operation_direction;
pub mod rect;
pub mod sizing;
pub use arrangement::*;
#[cfg(feature = "win32")]
pub use custom_layout::*;
pub use cycle_direction::*;
pub use default_layout::*;
pub use direction::*;
pub use layout::*;
pub use operation_direction::*;
pub use rect::*;
pub use sizing::*;
-31
View File
@@ -1,31 +0,0 @@
use clap::ValueEnum;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Sizing
pub enum Sizing {
/// Increase
Increase,
/// Decrease
Decrease,
}
impl Sizing {
#[must_use]
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
match self {
Self::Increase => value + adjustment,
Self::Decrease => {
if value > 0 && value - adjustment >= 0 {
value - adjustment
} else {
value
}
}
}
}
}
-11
View File
@@ -1,11 +0,0 @@
[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
View File
@@ -1,98 +0,0 @@
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();
}
+6 -14
View File
@@ -1,22 +1,14 @@
[package] [package]
name = "komorebi-themes" name = "komorebi-themes"
version = "0.1.40" version = "0.1.36"
edition = "2024" edition = "2021"
[dependencies] [dependencies]
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "b9e26b31f7a0e7ed239b14e5317e95d1bdc544bd" } base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "96f26c88d83781f234d42222293ec73d23a39ad8" }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] } catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "bdaff30959512c4f7ee7304117076a48633d777f", default-features = false, features = ["egui31"] }
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a", default-features = false, features = [ #catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
"egui33",
] }
eframe = { workspace = true } eframe = { workspace = true }
schemars = { workspace = true, optional = true } schemars = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_variant = "0.1" serde_variant = "0.1"
strum = { workspace = true } strum = { workspace = true }
hex_color = { version = "3", features = ["serde"] }
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
[features]
default = ["schemars"]
schemars = ["dep:schemars"]
-80
View File
@@ -1,80 +0,0 @@
use crate::Base16ColourPalette;
use crate::colour::Colour;
use crate::colour::Hex;
use hex_color::HexColor;
use std::collections::VecDeque;
use std::fmt::Display;
use std::fmt::Formatter;
use std::path::Path;
use serde::Deserialize;
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,
}
impl Display for ThemeVariant {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ThemeVariant::Dark => write!(f, "dark"),
ThemeVariant::Light => write!(f, "light"),
}
}
}
impl From<ThemeVariant> for flavours::operations::generate::Mode {
fn from(value: ThemeVariant) -> Self {
match value {
ThemeVariant::Dark => Self::Dark,
ThemeVariant::Light => Self::Light,
}
}
}
pub fn generate_base16_palette(
image_path: &Path,
variant: ThemeVariant,
) -> Result<Base16ColourPalette, hex_color::ParseHexColorError> {
Base16ColourPalette::try_from(
&flavours::operations::generate::generate(image_path, variant.into(), false)
.unwrap_or_default(),
)
}
impl TryFrom<&VecDeque<String>> for Base16ColourPalette {
type Error = hex_color::ParseHexColorError;
fn try_from(value: &VecDeque<String>) -> Result<Self, Self::Error> {
let fixed = value.iter().map(|s| format!("#{s}")).collect::<Vec<_>>();
if fixed.len() != 16 {
return Err(hex_color::ParseHexColorError::Empty);
}
Ok(Self {
base_00: Colour::Hex(Hex(HexColor::parse(&fixed[0])?)),
base_01: Colour::Hex(Hex(HexColor::parse(&fixed[1])?)),
base_02: Colour::Hex(Hex(HexColor::parse(&fixed[2])?)),
base_03: Colour::Hex(Hex(HexColor::parse(&fixed[3])?)),
base_04: Colour::Hex(Hex(HexColor::parse(&fixed[4])?)),
base_05: Colour::Hex(Hex(HexColor::parse(&fixed[5])?)),
base_06: Colour::Hex(Hex(HexColor::parse(&fixed[6])?)),
base_07: Colour::Hex(Hex(HexColor::parse(&fixed[7])?)),
base_08: Colour::Hex(Hex(HexColor::parse(&fixed[8])?)),
base_09: Colour::Hex(Hex(HexColor::parse(&fixed[9])?)),
base_0a: Colour::Hex(Hex(HexColor::parse(&fixed[10])?)),
base_0b: Colour::Hex(Hex(HexColor::parse(&fixed[11])?)),
base_0c: Colour::Hex(Hex(HexColor::parse(&fixed[12])?)),
base_0d: Colour::Hex(Hex(HexColor::parse(&fixed[13])?)),
base_0e: Colour::Hex(Hex(HexColor::parse(&fixed[14])?)),
base_0f: Colour::Hex(Hex(HexColor::parse(&fixed[15])?)),
})
}
}
+21 -537
View File
@@ -1,196 +1,30 @@
#![warn(clippy::all)] #![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_errors_doc)]
pub mod colour;
mod generator;
pub use generator::ThemeVariant;
pub use generator::generate_base16_palette;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use strum::Display; use strum::Display;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::colour::Colour;
pub use base16_egui_themes::Base16; pub use base16_egui_themes::Base16;
pub use catppuccin_egui; pub use catppuccin_egui;
pub use eframe::egui::Color32; 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; use serde_variant::to_variant_name;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "type")] #[serde(tag = "type")]
/// Theme
pub enum Theme { pub enum Theme {
/// Theme from catppuccin-egui /// A theme from catppuccin-egui
Catppuccin { Catppuccin {
name: Catppuccin, name: Catppuccin,
accent: Option<CatppuccinValue>, accent: Option<CatppuccinValue>,
}, },
/// Theme from base16-egui-themes /// A theme from base16-egui-themes
Base16 { Base16 {
name: Base16, name: Base16,
accent: Option<Base16Value>, accent: Option<Base16Value>,
}, },
/// Custom base16 palette
Custom {
palette: Box<Base16ColourPalette>,
accent: Option<Base16Value>,
},
}
#[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,
}
impl Base16ColourPalette {
pub fn background(self) -> Color32 {
self.base_01.into()
}
pub fn style(self) -> Style {
let original = Style::default();
Style {
visuals: Visuals {
widgets: Widgets {
noninteractive: WidgetVisuals {
bg_fill: self.base_01.into(),
weak_bg_fill: self.base_01.into(),
bg_stroke: Stroke {
color: self.base_02.into(),
..original.visuals.widgets.noninteractive.bg_stroke
},
fg_stroke: Stroke {
color: self.base_05.into(),
..original.visuals.widgets.noninteractive.fg_stroke
},
..original.visuals.widgets.noninteractive
},
inactive: WidgetVisuals {
bg_fill: self.base_02.into(),
weak_bg_fill: self.base_02.into(),
bg_stroke: Stroke {
color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
..original.visuals.widgets.inactive.bg_stroke
},
fg_stroke: Stroke {
color: self.base_05.into(),
..original.visuals.widgets.inactive.fg_stroke
},
..original.visuals.widgets.inactive
},
hovered: WidgetVisuals {
bg_fill: self.base_02.into(),
weak_bg_fill: self.base_02.into(),
bg_stroke: Stroke {
color: self.base_03.into(),
..original.visuals.widgets.hovered.bg_stroke
},
fg_stroke: Stroke {
color: self.base_06.into(),
..original.visuals.widgets.hovered.fg_stroke
},
..original.visuals.widgets.hovered
},
active: WidgetVisuals {
bg_fill: self.base_02.into(),
weak_bg_fill: self.base_02.into(),
bg_stroke: Stroke {
color: self.base_03.into(),
..original.visuals.widgets.hovered.bg_stroke
},
fg_stroke: Stroke {
color: self.base_06.into(),
..original.visuals.widgets.hovered.fg_stroke
},
..original.visuals.widgets.active
},
open: WidgetVisuals {
bg_fill: self.base_01.into(),
weak_bg_fill: self.base_01.into(),
bg_stroke: Stroke {
color: self.base_02.into(),
..original.visuals.widgets.open.bg_stroke
},
fg_stroke: Stroke {
color: self.base_06.into(),
..original.visuals.widgets.open.fg_stroke
},
..original.visuals.widgets.open
},
},
selection: Selection {
bg_fill: self.base_02.into(),
stroke: Stroke {
color: self.base_06.into(),
..original.visuals.selection.stroke
},
},
hyperlink_color: self.base_08.into(),
faint_bg_color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
extreme_bg_color: self.base_00.into(),
code_bg_color: self.base_02.into(),
warn_fg_color: self.base_0c.into(),
error_fg_color: self.base_0b.into(),
window_shadow: Shadow {
color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
..original.visuals.window_shadow
},
window_fill: self.base_01.into(),
window_stroke: Stroke {
color: self.base_02.into(),
..original.visuals.window_stroke
},
panel_fill: self.base_01.into(),
popup_shadow: Shadow {
color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
..original.visuals.popup_shadow
},
..original.visuals
},
..original
}
}
} }
impl Theme { impl Theme {
@@ -211,110 +45,59 @@ impl Theme {
.to_string() .to_string()
}) })
.collect(), .collect(),
Theme::Custom { .. } => vec!["Custom".to_string()],
} }
} }
} }
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)] #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
/// Base16 value
pub enum Base16Value { pub enum Base16Value {
/// Base00
Base00, Base00,
/// Base01
Base01, Base01,
/// Base02
Base02, Base02,
/// Base03
Base03, Base03,
/// Base04
Base04, Base04,
/// Base05
Base05, Base05,
/// Base06
#[default] #[default]
Base06, Base06,
/// Base07
Base07, Base07,
/// Base08
Base08, Base08,
/// Base09
Base09, Base09,
/// Base0A
Base0A, Base0A,
/// Base0B
Base0B, Base0B,
/// Base0C
Base0C, Base0C,
/// Base0D
Base0D, Base0D,
/// Base0E
Base0E, Base0E,
/// Base0F
Base0F, Base0F,
} }
/// Wrapper around a Base16 colour palette
pub enum Base16Wrapper {
/// Predefined Base16 colour palette
Base16(Base16),
/// Custom Base16 colour palette
Custom(Box<Base16ColourPalette>),
}
impl Base16Value { impl Base16Value {
pub fn color32(&self, theme: Base16Wrapper) -> Color32 { pub fn color32(&self, theme: Base16) -> Color32 {
match theme { match self {
Base16Wrapper::Base16(theme) => match self { Base16Value::Base00 => theme.base00(),
Base16Value::Base00 => theme.base00(), Base16Value::Base01 => theme.base01(),
Base16Value::Base01 => theme.base01(), Base16Value::Base02 => theme.base02(),
Base16Value::Base02 => theme.base02(), Base16Value::Base03 => theme.base03(),
Base16Value::Base03 => theme.base03(), Base16Value::Base04 => theme.base04(),
Base16Value::Base04 => theme.base04(), Base16Value::Base05 => theme.base05(),
Base16Value::Base05 => theme.base05(), Base16Value::Base06 => theme.base06(),
Base16Value::Base06 => theme.base06(), Base16Value::Base07 => theme.base07(),
Base16Value::Base07 => theme.base07(), Base16Value::Base08 => theme.base08(),
Base16Value::Base08 => theme.base08(), Base16Value::Base09 => theme.base09(),
Base16Value::Base09 => theme.base09(), Base16Value::Base0A => theme.base0a(),
Base16Value::Base0A => theme.base0a(), Base16Value::Base0B => theme.base0b(),
Base16Value::Base0B => theme.base0b(), Base16Value::Base0C => theme.base0c(),
Base16Value::Base0C => theme.base0c(), Base16Value::Base0D => theme.base0d(),
Base16Value::Base0D => theme.base0d(), Base16Value::Base0E => theme.base0e(),
Base16Value::Base0E => theme.base0e(), Base16Value::Base0F => theme.base0f(),
Base16Value::Base0F => theme.base0f(),
},
Base16Wrapper::Custom(colours) => match self {
Base16Value::Base00 => colours.base_00.into(),
Base16Value::Base01 => colours.base_01.into(),
Base16Value::Base02 => colours.base_02.into(),
Base16Value::Base03 => colours.base_03.into(),
Base16Value::Base04 => colours.base_04.into(),
Base16Value::Base05 => colours.base_05.into(),
Base16Value::Base06 => colours.base_06.into(),
Base16Value::Base07 => colours.base_07.into(),
Base16Value::Base08 => colours.base_08.into(),
Base16Value::Base09 => colours.base_09.into(),
Base16Value::Base0A => colours.base_0a.into(),
Base16Value::Base0B => colours.base_0b.into(),
Base16Value::Base0C => colours.base_0c.into(),
Base16Value::Base0D => colours.base_0d.into(),
Base16Value::Base0E => colours.base_0e.into(),
Base16Value::Base0F => colours.base_0f.into(),
},
} }
} }
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
/// Catppuccin palette
pub enum Catppuccin { pub enum Catppuccin {
/// Frappe (https://catppuccin.com/palette#flavor-frappe)
Frappe, Frappe,
/// Latte (https://catppuccin.com/palette#flavor-latte)
Latte, Latte,
/// Macchiato (https://catppuccin.com/palette#flavor-macchiato)
Macchiato, Macchiato,
/// Mocha (https://catppuccin.com/palette#flavor-mocha)
Mocha, Mocha,
} }
@@ -336,60 +119,33 @@ impl From<Catppuccin> for catppuccin_egui::Theme {
} }
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)] #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
/// Catppuccin Value
pub enum CatppuccinValue { pub enum CatppuccinValue {
/// Rosewater
Rosewater, Rosewater,
/// Flamingo
Flamingo, Flamingo,
/// Pink
Pink, Pink,
/// Mauve
Mauve, Mauve,
/// Red
Red, Red,
/// Maroon
Maroon, Maroon,
/// Peach
Peach, Peach,
/// Yellow
Yellow, Yellow,
/// Green
Green, Green,
/// Teal
Teal, Teal,
/// Sky
Sky, Sky,
/// Sapphire
Sapphire, Sapphire,
/// Blue
Blue, Blue,
/// Lavender
Lavender, Lavender,
#[default] #[default]
/// Text
Text, Text,
/// Subtext1
Subtext1, Subtext1,
/// Subtext0
Subtext0, Subtext0,
/// Overlay2
Overlay2, Overlay2,
/// Overlay1
Overlay1, Overlay1,
/// Overlay0
Overlay0, Overlay0,
/// Surface2
Surface2, Surface2,
/// Surface1
Surface1, Surface1,
/// Surface0
Surface0, Surface0,
/// Base
Base, Base,
/// Mantle
Mantle, Mantle,
/// Crust
Crust, Crust,
} }
@@ -429,275 +185,3 @@ 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,
}),
}
}
}
+9 -13
View File
@@ -1,40 +1,37 @@
[package] [package]
name = "komorebi" name = "komorebi"
version = "0.1.40" version = "0.1.36"
description = "A tiling window manager for Windows" description = "A tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi" repository = "https://github.com/LGUG2Z/komorebi"
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
komorebi-layouts = { path = "../komorebi-layouts", features = ["win32"] }
komorebi-themes = { path = "../komorebi-themes" } komorebi-themes = { path = "../komorebi-themes" }
base64 = "0.22"
bitflags = { version = "2", features = ["serde"] } bitflags = { version = "2", features = ["serde"] }
clap = { workspace = true } clap = { workspace = true }
chrono = { workspace = true }
color-eyre = { workspace = true } color-eyre = { workspace = true }
crossbeam-channel = { workspace = true } crossbeam-channel = { workspace = true }
crossbeam-utils = { workspace = true } crossbeam-utils = { workspace = true }
ctrlc = { version = "3", features = ["termination"] } ctrlc = { version = "3", features = ["termination"] }
dirs = { workspace = true } dirs = { workspace = true }
ed25519-dalek = "2" dunce = { workspace = true }
getset = "0.1"
hex_color = { version = "3", features = ["serde"] }
hotwatch = { workspace = true } hotwatch = { workspace = true }
lazy_static = { workspace = true } lazy_static = { workspace = true }
miow = "0.6" miow = "0.6"
nanoid = "0.4" nanoid = "0.4"
net2 = "0.2" net2 = "0.2"
os_info = "3.10" os_info = "3.10"
parking_lot = { workspace = true } parking_lot = "0.12"
paste = { workspace = true } paste = { workspace = true }
powershell_script = "1.0"
regex = "1" regex = "1"
reqwest = { version = "0.13", features = ["blocking"] }
schemars = { workspace = true, optional = true } schemars = { workspace = true, optional = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true, features = ["preserve_order"] } serde_json = { workspace = true }
serde_yaml = { workspace = true } serde_yaml = { workspace = true }
shadow-rs = { workspace = true } shadow-rs = { workspace = true }
strum = { workspace = true } strum = { workspace = true }
@@ -52,16 +49,15 @@ windows-implement = { workspace = true }
windows-interface = { workspace = true } windows-interface = { workspace = true }
winput = "0.2" winput = "0.2"
winreg = "0.55" winreg = "0.55"
serde_with = { version = "3.12", features = ["schemars_1"] }
[build-dependencies] [build-dependencies]
shadow-rs = { workspace = true } shadow-rs = { workspace = true }
[dev-dependencies] [dev-dependencies]
reqwest = { version = "0.13", features = ["blocking"] } reqwest = { version = "0.12", features = ["blocking"] }
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
[features] [features]
default = ["schemars"] default = ["schemars"]
deadlock_detection = ["parking_lot/deadlock_detection"] deadlock_detection = ["parking_lot/deadlock_detection"]
schemars = ["dep:schemars", "komorebi-layouts/schemars"] schemars = ["dep:schemars"]
+1 -1
View File
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::collections::HashMap;
use super::prefix::AnimationPrefix; use super::prefix::AnimationPrefix;
+4 -4
View File
@@ -1,4 +1,4 @@
use color_eyre::eyre; use color_eyre::Result;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
@@ -6,10 +6,10 @@ use std::sync::atomic::Ordering;
use std::time::Duration; use std::time::Duration;
use std::time::Instant; use std::time::Instant;
use super::RenderDispatcher;
use super::ANIMATION_DURATION_GLOBAL; use super::ANIMATION_DURATION_GLOBAL;
use super::ANIMATION_FPS; use super::ANIMATION_FPS;
use super::ANIMATION_MANAGER; use super::ANIMATION_MANAGER;
use super::RenderDispatcher;
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)] #[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@@ -55,9 +55,9 @@ impl AnimationEngine {
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
pub fn animate( pub fn animate(
render_dispatcher: impl RenderDispatcher + Send + 'static, render_dispatcher: (impl RenderDispatcher + Send + 'static),
duration: Duration, duration: Duration,
) -> eyre::Result<()> { ) -> Result<()> {
std::thread::spawn(move || { std::thread::spawn(move || {
let animation_key = render_dispatcher.get_animation_key(); let animation_key = render_dispatcher.get_animation_key();
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) { if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::AnimationStyle;
use crate::core::Rect; use crate::core::Rect;
use crate::AnimationStyle;
use super::style::apply_ease_func; use super::style::apply_ease_func;
+1 -26
View File
@@ -4,9 +4,9 @@ use crate::core::animation::AnimationStyle;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use prefix::AnimationPrefix; use prefix::AnimationPrefix;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64; use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use parking_lot::Mutex; use parking_lot::Mutex;
@@ -25,33 +25,8 @@ use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)] #[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> { pub enum PerAnimationPrefixConfig<T> {
/// Animation configuration prefixed for a specific animation kind
Prefix(HashMap<AnimationPrefix, T>), Prefix(HashMap<AnimationPrefix, T>),
/// Animation configuration for all animation kinds
Global(T), Global(T),
} }
+1 -1
View File
@@ -17,5 +17,5 @@ pub enum AnimationPrefix {
} }
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String { pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
format!("{prefix}:{key}") format!("{}:{}", prefix, key)
} }
+4 -4
View File
@@ -1,8 +1,8 @@
use color_eyre::eyre; use color_eyre::Result;
pub trait RenderDispatcher { pub trait RenderDispatcher {
fn get_animation_key(&self) -> String; fn get_animation_key(&self) -> String;
fn pre_render(&self) -> eyre::Result<()>; fn pre_render(&self) -> Result<()>;
fn render(&self, delta: f64) -> eyre::Result<()>; fn render(&self, delta: f64) -> Result<()>;
fn post_render(&self) -> eyre::Result<()>; fn post_render(&self) -> Result<()>;
} }
-57
View File
@@ -355,61 +355,6 @@ 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 { pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
match style { match style {
AnimationStyle::Linear => Linear::evaluate(t), AnimationStyle::Linear => Linear::evaluate(t),
@@ -420,7 +365,6 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t), AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t), AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t), AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t),
AnimationStyle::EaseOutCubic => EaseOutCubic::evaluate(t),
AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t), AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t), AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t),
AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t), AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t),
@@ -443,6 +387,5 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t), AnimationStyle::EaseInBounce => EaseInBounce::evaluate(t),
AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t), AnimationStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t), AnimationStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
AnimationStyle::CubicBezier(x1, y1, x2, y2) => CubicBezier { x1, y1, x2, y2 }.evaluate(t),
} }
} }
+97 -134
View File
@@ -1,32 +1,35 @@
use crate::WINDOWS_11; use crate::border_manager::window_kind_colour;
use crate::WindowsApi; use crate::border_manager::RenderTarget;
use crate::border_manager::WindowKind;
use crate::border_manager::BORDER_OFFSET; use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH; use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::RenderTarget;
use crate::border_manager::STYLE; use crate::border_manager::STYLE;
use crate::border_manager::WindowKind;
use crate::border_manager::window_kind_colour;
use crate::core::BorderStyle; use crate::core::BorderStyle;
use crate::core::Rect; use crate::core::Rect;
use crate::windows_api; use crate::windows_api;
use crate::WindowsApi;
use crate::WINDOWS_11;
use color_eyre::eyre::anyhow;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::LazyLock;
use std::sync::OnceLock;
use windows::Win32::Foundation::FALSE; use windows::Win32::Foundation::FALSE;
use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM; use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT; use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::TRUE; use windows::Win32::Foundation::TRUE;
use windows::Win32::Foundation::WPARAM; 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_ALPHA_MODE_PREMULTIPLIED;
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F; use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT; 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_ANTIALIAS_MODE_PER_PRIMITIVE;
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES; use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED; use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
@@ -35,34 +38,31 @@ 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_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT; use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT; use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory; use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
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_BLURREGION;
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE; use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND; 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::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
use windows::Win32::Graphics::Gdi::CreateRectRgn; use windows::Win32::Graphics::Gdi::CreateRectRgn;
use windows::Win32::Graphics::Gdi::InvalidateRect; use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::Graphics::Gdi::ValidateRect; use windows::Win32::Graphics::Gdi::ValidateRect;
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW; use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW; use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW; use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics; use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW; use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW; use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage; use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
use windows::Win32::UI::WindowsAndMessaging::SetCursor; use windows::Win32::UI::WindowsAndMessaging::SetCursor;
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW; use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage; 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::IDC_ARROW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE; use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY; use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT; use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
@@ -104,10 +104,10 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) }; let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
let hwnd = hwnd.0 as isize; let hwnd = hwnd.0 as isize;
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
&& class.starts_with("komoborder") if class.starts_with("komoborder") {
{ hwnds.push(hwnd);
hwnds.push(hwnd); }
} }
true.into() true.into()
@@ -118,7 +118,7 @@ pub struct Border {
pub hwnd: isize, pub hwnd: isize,
pub id: String, pub id: String,
pub monitor_idx: Option<usize>, pub monitor_idx: Option<usize>,
pub render_target: Option<RenderTarget>, pub render_target: OnceLock<RenderTarget>,
pub tracking_hwnd: isize, pub tracking_hwnd: isize,
pub window_rect: Rect, pub window_rect: Rect,
pub window_kind: WindowKind, pub window_kind: WindowKind,
@@ -128,7 +128,6 @@ pub struct Border {
pub brush_properties: D2D1_BRUSH_PROPERTIES, pub brush_properties: D2D1_BRUSH_PROPERTIES,
pub rounded_rect: D2D1_ROUNDED_RECT, pub rounded_rect: D2D1_ROUNDED_RECT,
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>, pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
pub is_destroying: Arc<AtomicBool>,
} }
impl From<isize> for Border { impl From<isize> for Border {
@@ -137,7 +136,7 @@ impl From<isize> for Border {
hwnd: value, hwnd: value,
id: String::new(), id: String::new(),
monitor_idx: None, monitor_idx: None,
render_target: None, render_target: OnceLock::new(),
tracking_hwnd: 0, tracking_hwnd: 0,
window_rect: Rect::default(), window_rect: Rect::default(),
window_kind: WindowKind::Unfocused, window_kind: WindowKind::Unfocused,
@@ -147,7 +146,6 @@ impl From<isize> for Border {
brush_properties: D2D1_BRUSH_PROPERTIES::default(), brush_properties: D2D1_BRUSH_PROPERTIES::default(),
rounded_rect: D2D1_ROUNDED_RECT::default(), rounded_rect: D2D1_ROUNDED_RECT::default(),
brushes: HashMap::new(), brushes: HashMap::new(),
is_destroying: Arc::new(AtomicBool::new(false)),
} }
} }
} }
@@ -186,7 +184,7 @@ impl Border {
hwnd: 0, hwnd: 0,
id: container_id, id: container_id,
monitor_idx: Some(monitor_idx), monitor_idx: Some(monitor_idx),
render_target: None, render_target: OnceLock::new(),
tracking_hwnd, tracking_hwnd,
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(), window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
window_kind: WindowKind::Unfocused, window_kind: WindowKind::Unfocused,
@@ -196,7 +194,6 @@ impl Border {
brush_properties: Default::default(), brush_properties: Default::default(),
rounded_rect: Default::default(), rounded_rect: Default::default(),
brushes: HashMap::new(), brushes: HashMap::new(),
is_destroying: Arc::new(AtomicBool::new(false)),
}; };
let border_pointer = &raw mut border; let border_pointer = &raw mut border;
@@ -246,14 +243,8 @@ impl Border {
let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh); let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh);
} }
border.update_brushes()?;
Ok(border)
}
pub fn update_brushes(&mut self) -> color_eyre::Result<()> {
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES { let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
hwnd: HWND(windows_api::as_ptr!(self.hwnd)), hwnd: HWND(windows_api::as_ptr!(border.hwnd)),
pixelSize: Default::default(), pixelSize: Default::default(),
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY, presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
}; };
@@ -274,7 +265,7 @@ impl Border {
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties) .CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
} { } {
Ok(render_target) => unsafe { Ok(render_target) => unsafe {
self.brush_properties = *BRUSH_PROPERTIES.deref(); border.brush_properties = *BRUSH_PROPERTIES.deref();
for window_kind in [ for window_kind in [
WindowKind::Single, WindowKind::Single,
WindowKind::Stack, WindowKind::Stack,
@@ -292,18 +283,24 @@ impl Border {
}; };
if let Ok(brush) = if let Ok(brush) =
render_target.CreateSolidColorBrush(&color, Some(&self.brush_properties)) render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties))
{ {
self.brushes.insert(window_kind, brush); border.brushes.insert(window_kind, brush);
} }
} }
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
self.render_target = Some(RenderTarget(render_target)); if border
.render_target
.set(RenderTarget(render_target.clone()))
.is_err()
{
return Err(anyhow!("could not store border render target"));
}
self.rounded_rect = { border.rounded_rect = {
let radius = 8.0 + self.width as f32 / 2.0; let radius = 8.0 + border.width as f32 / 2.0;
D2D1_ROUNDED_RECT { D2D1_ROUNDED_RECT {
rect: Default::default(), rect: Default::default(),
radiusX: radius, radiusX: radius,
@@ -311,24 +308,13 @@ impl Border {
} }
}; };
Ok(()) Ok(border)
}, },
Err(error) => Err(error.into()), Err(error) => Err(error.into()),
} }
} }
pub fn destroy(&self) -> color_eyre::Result<()> { pub fn destroy(&self) -> color_eyre::Result<()> {
// signal that we're destroying - prevents new render operations
self.is_destroying.store(true, Ordering::Release);
// small delay to allow in-flight render operations to complete
std::thread::sleep(std::time::Duration::from_millis(10));
// 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) WindowsApi::close_window(self.hwnd)
} }
@@ -397,10 +383,6 @@ impl Border {
return LRESULT(0); return LRESULT(0);
} }
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
return LRESULT(0);
}
let reference_hwnd = (*border_pointer).tracking_hwnd; let reference_hwnd = (*border_pointer).tracking_hwnd;
let old_rect = (*border_pointer).window_rect; let old_rect = (*border_pointer).window_rect;
@@ -412,68 +394,63 @@ impl Border {
tracing::error!("failed to update border position {error}"); tracing::error!("failed to update border position {error}");
} }
if (!rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect)) if !rect.is_same_size_as(&old_rect) {
&& let Some(render_target) = (*border_pointer).render_target.as_ref() if let Some(render_target) = (*border_pointer).render_target.get() {
{ let border_width = (*border_pointer).width;
// double-check destruction flag before rendering let border_offset = (*border_pointer).offset;
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
return LRESULT(0);
}
let border_width = (*border_pointer).width; (*border_pointer).rounded_rect.rect = D2D_RECT_F {
let border_offset = (*border_pointer).offset; left: (border_width / 2 - border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
(*border_pointer).rounded_rect.rect = D2D_RECT_F { right: (rect.right - border_width / 2 + border_offset) as f32,
left: (border_width / 2 - border_offset) as f32, bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
right: (rect.right - border_width / 2 + border_offset) as f32,
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
};
let _ = render_target.Resize(&D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
});
let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
render_target.BeginDraw();
render_target.Clear(None);
// Calculate border radius based on style
let style = match (*border_pointer).style {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
} else {
BorderStyle::Square
}
}
BorderStyle::Rounded => BorderStyle::Rounded,
BorderStyle::Square => BorderStyle::Square,
}; };
match style { let _ = render_target.Resize(&D2D_SIZE_U {
BorderStyle::Rounded => { width: rect.right as u32,
render_target.DrawRoundedRectangle( height: rect.bottom as u32,
&(*border_pointer).rounded_rect, });
brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
render_target.DrawRectangle(
&(*border_pointer).rounded_rect.rect,
brush,
border_width as f32,
None,
);
}
_ => {}
}
let _ = render_target.EndDraw(None, None); let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
render_target.BeginDraw();
render_target.Clear(None);
// Calculate border radius based on style
let style = match (*border_pointer).style {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
} else {
BorderStyle::Square
}
}
BorderStyle::Rounded => BorderStyle::Rounded,
BorderStyle::Square => BorderStyle::Square,
};
match style {
BorderStyle::Rounded => {
render_target.DrawRoundedRectangle(
&(*border_pointer).rounded_rect,
brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
render_target.DrawRectangle(
&(*border_pointer).rounded_rect.rect,
brush,
border_width as f32,
None,
);
}
_ => {}
}
let _ = render_target.EndDraw(None, None);
}
} }
} }
@@ -488,10 +465,6 @@ impl Border {
return LRESULT(0); return LRESULT(0);
} }
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
return LRESULT(0);
}
let reference_hwnd = (*border_pointer).tracking_hwnd; let reference_hwnd = (*border_pointer).tracking_hwnd;
// Update position to update the ZOrder // Update position to update the ZOrder
@@ -504,12 +477,7 @@ impl Border {
tracing::error!("failed to update border position {error}"); tracing::error!("failed to update border position {error}");
} }
if let Some(render_target) = (*border_pointer).render_target.as_ref() { if let Some(render_target) = (*border_pointer).render_target.get() {
// double-check destruction flag before rendering
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
return LRESULT(0);
}
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed); (*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed); (*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
@@ -577,12 +545,7 @@ impl Border {
LRESULT(0) LRESULT(0)
} }
WM_DESTROY => { WM_DESTROY => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _; SetWindowLongPtrW(window, GWLP_USERDATA, 0);
if !border_pointer.is_null() {
(*border_pointer).render_target = None;
(*border_pointer).brushes.clear();
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
}
PostQuitMessage(0); PostQuitMessage(0);
LRESULT(0) LRESULT(0)
} }
+150 -257
View File
@@ -1,36 +1,35 @@
#![deny(clippy::unwrap_used, clippy::expect_used)] #![deny(clippy::unwrap_used, clippy::expect_used)]
mod border; mod border;
use crate::WindowManager;
use crate::WindowsApi;
use crate::core::BorderImplementation; use crate::core::BorderImplementation;
use crate::core::BorderStyle; use crate::core::BorderStyle;
use crate::core::WindowKind; use crate::core::WindowKind;
use crate::ring::Ring; use crate::ring::Ring;
use crate::windows_api; use crate::windows_api;
use crate::workspace::Workspace;
use crate::workspace::WorkspaceLayer; use crate::workspace::WorkspaceLayer;
pub use border::Border; use crate::Colour;
use crate::Rgb;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds; use border::border_hwnds;
pub use border::Border;
use crossbeam_channel::Receiver; use crossbeam_channel::Receiver;
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell; use crossbeam_utils::atomic::AtomicCell;
use crossbeam_utils::atomic::AtomicConsume; use crossbeam_utils::atomic::AtomicConsume;
use komorebi_themes::colour::Colour;
use komorebi_themes::colour::Rgb;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap;
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32; use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32; use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use strum::Display; use strum::Display;
use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget; use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
@@ -74,10 +73,7 @@ impl Deref for RenderTarget {
} }
} }
pub enum Notification { pub struct Notification(pub Option<isize>);
Update(Option<isize>),
ForceUpdate,
}
#[derive(Debug, Default, Clone, Copy, PartialEq)] #[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct BorderInfo { pub struct BorderInfo {
@@ -106,21 +102,16 @@ fn event_rx() -> Receiver<Notification> {
} }
pub fn window_border(hwnd: isize) -> Option<BorderInfo> { pub fn window_border(hwnd: isize) -> Option<BorderInfo> {
let id = WINDOWS_BORDERS.lock().get(&hwnd)?.clone(); WINDOWS_BORDERS.lock().get(&hwnd).and_then(|id| {
BORDER_STATE.lock().get(&id).map(|b| BorderInfo { BORDER_STATE.lock().get(id).map(|b| BorderInfo {
border_hwnd: b.hwnd, border_hwnd: b.hwnd,
window_kind: b.window_kind, window_kind: b.window_kind,
})
}) })
} }
pub fn send_notification(hwnd: Option<isize>) { pub fn send_notification(hwnd: Option<isize>) {
if event_tx().try_send(Notification::Update(hwnd)).is_err() { if event_tx().try_send(Notification(hwnd)).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn send_force_update() {
if event_tx().try_send(Notification::ForceUpdate).is_err() {
tracing::warn!("channel is full; dropping notification") tracing::warn!("channel is full; dropping notification")
} }
} }
@@ -136,8 +127,6 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
let _ = destroy_border(border); let _ = destroy_border(border);
} }
drop(borders);
WINDOWS_BORDERS.lock().clear(); WINDOWS_BORDERS.lock().clear();
let mut remaining_hwnds = vec![]; let mut remaining_hwnds = vec![];
@@ -170,15 +159,13 @@ fn window_kind_colour(focus_kind: WindowKind) -> u32 {
} }
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) { pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || { std::thread::spawn(move || loop {
loop { match handle_notifications(wm.clone()) {
match handle_notifications(wm.clone()) { Ok(()) => {
Ok(()) => { tracing::warn!("restarting finished thread");
tracing::warn!("restarting finished thread"); }
} Err(error) => {
Err(error) => { tracing::warn!("restarting failed thread: {}", error);
tracing::warn!("restarting failed thread: {}", error);
}
} }
} }
}); });
@@ -188,7 +175,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
tracing::info!("listening"); tracing::info!("listening");
let receiver = event_rx(); let receiver = event_rx();
event_tx().send(Notification::Update(None))?; event_tx().send(Notification(None))?;
let mut previous_snapshot = Ring::default(); let mut previous_snapshot = Ring::default();
let mut previous_pending_move_op = None; let mut previous_pending_move_op = None;
@@ -211,12 +198,10 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
.iter() .iter()
.map(|w| w.hwnd) .map(|w| w.hwnd)
.collect::<Vec<_>>(); .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] [focused_workspace_idx]
.layer; .layer();
let foreground_window = WindowsApi::foreground_window().unwrap_or_default(); 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); drop(state);
@@ -226,7 +211,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Only operate on the focused workspace of each monitor // Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() { if let Some(ws) = m.focused_workspace() {
// Handle the monocle container separately // 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 { let window_kind = if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused WindowKind::Unfocused
} else { } else {
@@ -239,17 +224,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
.unwrap_or_default() .unwrap_or_default()
.set_accent(window_kind_colour(window_kind))?; .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; continue 'monitors;
} }
@@ -257,7 +231,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let window_kind = if idx != ws.focused_container_idx() let window_kind = if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx || monitor_idx != focused_monitor_idx
{ {
if c.locked { if ws.locked_containers().contains(&idx) {
WindowKind::UnfocusedLocked WindowKind::UnfocusedLocked
} else { } else {
WindowKind::Unfocused WindowKind::Unfocused
@@ -287,74 +261,67 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
} }
} }
BorderImplementation::Komorebi => { BorderImplementation::Komorebi => {
let should_process_notification = match notification { let mut should_process_notification = true;
Notification::Update(notification_hwnd) => {
let mut should_process_notification = true;
if monitors == previous_snapshot if monitors == previous_snapshot
// handle the window dragging edge case // handle the window dragging edge case
&& pending_move_op == previous_pending_move_op && pending_move_op == previous_pending_move_op
{ {
should_process_notification = false; should_process_notification = false;
} }
// handle the pause edge case // handle the pause edge case
if is_paused && !previous_is_paused { if is_paused && !previous_is_paused {
should_process_notification = true; should_process_notification = true;
} }
// handle the unpause edge case // handle the unpause edge case
if previous_is_paused && !is_paused { if previous_is_paused && !is_paused {
should_process_notification = true; should_process_notification = true;
} }
// handle the retile edge case // handle the retile edge case
if !should_process_notification && BORDER_STATE.lock().is_empty() { if !should_process_notification && BORDER_STATE.lock().is_empty() {
should_process_notification = true; should_process_notification = true;
} }
// when we switch focus to/from a floating window // when we switch focus to/from a floating window
let switch_focus_to_from_floating_window = let switch_focus_to_from_floating_window = floating_window_hwnds.iter().any(|fw| {
floating_window_hwnds.iter().any(|fw| { // if we switch focus to a floating window
// if we switch focus to a floating window fw == &notification.0.unwrap_or_default() ||
fw == &notification_hwnd.unwrap_or_default() || // if there is any floating window with a `WindowKind::Floating` border
// if there is any floating window with a `WindowKind::Floating` border // that no longer is the foreground window then we need to update that
// that no longer is the foreground window then we need to update that // border.
// border. (fw != &foreground_window
(fw != &foreground_window && window_border(*fw)
&& window_border(*fw) .is_some_and(|b| b.window_kind == WindowKind::Floating))
.is_some_and(|b| b.window_kind == WindowKind::Floating)) });
});
// when the focused window has an `Unfocused` border kind, usually this happens if // when the focused window has an `Unfocused` border kind, usually this happens if
// we focus an admin window and then refocus the previously focused window. For // we focus an admin window and then refocus the previously focused window. For
// komorebi it will have the same state has before, however the previously focused // komorebi it will have the same state has before, however the previously focused
// window changed its border to unfocused so now we need to update it again. // window changed its border to unfocused so now we need to update it again.
if !should_process_notification if !should_process_notification
&& window_border(notification_hwnd.unwrap_or_default()) && window_border(notification.0.unwrap_or_default())
.is_some_and(|b| b.window_kind == WindowKind::Unfocused) .is_some_and(|b| b.window_kind == WindowKind::Unfocused)
{ {
should_process_notification = true; should_process_notification = true;
} }
if !should_process_notification && switch_focus_to_from_floating_window { if !should_process_notification && switch_focus_to_from_floating_window {
should_process_notification = true; 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
}
Notification::ForceUpdate => true,
};
if !should_process_notification { if !should_process_notification {
tracing::debug!("monitor state matches latest snapshot, skipping notification"); if let Some(ref previous) = previous_notification {
if previous.0.unwrap_or_default() != notification.0.unwrap_or_default() {
should_process_notification = true;
}
}
}
if !should_process_notification {
tracing::trace!("monitor state matches latest snapshot, skipping notification");
continue 'receiver; continue 'receiver;
} }
@@ -381,7 +348,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Only operate on the focused workspace of each monitor // Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() { if let Some(ws) = m.focused_workspace() {
// Workspaces with tiling disabled don't have borders // Workspaces with tiling disabled don't have borders
if !ws.tile { if !ws.tile() {
// Remove all borders on this monitor // Remove all borders on this monitor
remove_borders( remove_borders(
&mut borders, &mut borders,
@@ -394,16 +361,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
} }
// Handle the monocle container separately // 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 mut new_border = false;
let focused_window_hwnd = let focused_window_hwnd =
monocle.focused_window().map(|w| w.hwnd).unwrap_or_default(); 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()) { let border = match borders.entry(id.clone()) {
Entry::Occupied(entry) => entry.into_mut(), Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => { Entry::Vacant(entry) => {
if let Ok(border) = Border::create( if let Ok(border) = Border::create(
&monocle.id, monocle.id(),
focused_window_hwnd, focused_window_hwnd,
monitor_idx, monitor_idx,
) { ) {
@@ -448,11 +415,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
if new_border { if new_border {
border.set_position(&rect, focused_window_hwnd)?; border.set_position(&rect, focused_window_hwnd)?;
} else if matches!(notification, Notification::ForceUpdate) {
// 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.invalidate(); border.invalidate();
@@ -460,48 +422,22 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
windows_borders.insert(focused_window_hwnd, id); windows_borders.insert(focused_window_hwnd, id);
let border_hwnd = border.hwnd; 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; continue 'monitors;
} }
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default(); let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
let foreground_monitor_id = let foreground_monitor_id =
WindowsApi::monitor_from_window(foreground_hwnd); WindowsApi::monitor_from_window(foreground_hwnd);
let is_maximized = let is_maximized = foreground_monitor_id == m.id()
foreground_monitor_id == m.id && WindowsApi::is_zoomed(foreground_hwnd); && WindowsApi::is_zoomed(foreground_hwnd);
if is_maximized { if is_maximized {
// Remove all borders on this monitor // Remove all borders on this monitor
@@ -519,7 +455,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let mut container_and_floating_window_ids = ws let mut container_and_floating_window_ids = ws
.containers() .containers()
.iter() .iter()
.map(|c| c.id.clone()) .map(|c| c.id().clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for w in ws.floating_windows() { for w in ws.floating_windows() {
@@ -537,7 +473,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
'containers: for (idx, c) in ws.containers().iter().enumerate() { 'containers: for (idx, c) in ws.containers().iter().enumerate() {
let focused_window_hwnd = let focused_window_hwnd =
c.focused_window().map(|w| w.hwnd).unwrap_or_default(); 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 // Get the border entry for this container from the map or create one
let mut new_border = false; let mut new_border = false;
@@ -545,7 +481,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
Entry::Occupied(entry) => entry.into_mut(), Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => { Entry::Vacant(entry) => {
if let Ok(border) = 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; new_border = true;
entry.insert(border) entry.insert(border)
@@ -561,7 +497,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|| monitor_idx != focused_monitor_idx || monitor_idx != focused_monitor_idx
|| focused_window_hwnd != foreground_window || focused_window_hwnd != foreground_window
{ {
if c.locked { if ws.locked_containers().contains(&idx) {
WindowKind::UnfocusedLocked WindowKind::UnfocusedLocked
} else { } else {
WindowKind::Unfocused WindowKind::Unfocused
@@ -601,24 +537,19 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let rect = match WindowsApi::window_rect(focused_window_hwnd) { let rect = match WindowsApi::window_rect(focused_window_hwnd) {
Ok(rect) => rect, Ok(rect) => rect,
Err(_) => { Err(_) => {
remove_border(&c.id, &mut borders, &mut windows_borders)?; remove_border(c.id(), &mut borders, &mut windows_borders)?;
continue 'containers; continue 'containers;
} }
}; };
border.window_rect = rect; border.window_rect = rect;
let layer_changed = previous_layer != workspace_layer;
let should_invalidate = new_border let should_invalidate = new_border
|| (last_focus_state != new_focus_state) || (last_focus_state != new_focus_state)
|| layer_changed || layer_changed;
|| forced_update;
if should_invalidate { 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, focused_window_hwnd)?; border.set_position(&rect, focused_window_hwnd)?;
border.invalidate(); border.invalidate();
} }
@@ -626,15 +557,56 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
windows_borders.insert(focused_window_hwnd, id); windows_borders.insert(focused_window_hwnd, id);
} }
handle_floating_borders( {
&mut borders, for window in ws.floating_windows() {
&mut windows_borders, let mut new_border = false;
ws, let id = window.hwnd.to_string();
monitor_idx, let border = match borders.entry(id.clone()) {
foreground_window, Entry::Occupied(entry) => entry.into_mut(),
layer_changed, Entry::Vacant(entry) => {
forced_update, 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 should_invalidate = new_border
|| (last_focus_state != new_focus_state)
|| layer_changed;
if should_invalidate {
border.set_position(&rect, window.hwnd)?;
border.invalidate();
}
windows_borders.insert(window.hwnd, id);
}
}
} }
} }
} }
@@ -650,68 +622,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
Ok(()) 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 /// Removes all borders from monitor with index `monitor_idx` filtered by
/// `condition`. This condition is a function that will take a reference to /// `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 /// the container id and the border and returns a bool, if true that border
@@ -767,7 +677,6 @@ fn remove_border(
fn destroy_border(border: Box<Border>) -> color_eyre::Result<()> { fn destroy_border(border: Box<Border>) -> color_eyre::Result<()> {
let raw_pointer = Box::into_raw(border); let raw_pointer = Box::into_raw(border);
unsafe { unsafe {
// Now safe to destroy window
(*raw_pointer).destroy()?; (*raw_pointer).destroy()?;
} }
Ok(()) Ok(())
@@ -814,26 +723,10 @@ pub fn hide_border(tracking_hwnd: isize) {
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, PartialEq)] #[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[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 { pub enum ZOrder {
/// HWND_TOP
///
/// Places the window at the top of the Z order.
Top, 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, 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, Bottom,
/// HWND_TOPMOST
///
/// Places the window above all non-topmost windows.
/// The window maintains its topmost position even when it is deactivated.
TopMost, TopMost,
} }
@@ -1,17 +1,20 @@
use hex_color::HexColor; use hex_color::HexColor;
use komorebi_themes::Color32;
#[cfg(feature = "schemars")] #[cfg(feature = "schemars")]
use schemars::Schema; use schemars::gen::SchemaGenerator;
#[cfg(feature = "schemars")] #[cfg(feature = "schemars")]
use schemars::SchemaGenerator; use schemars::schema::InstanceType;
#[cfg(feature = "schemars")]
use schemars::schema::Schema;
#[cfg(feature = "schemars")]
use schemars::schema::SchemaObject;
use crate::Color32;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)] #[serde(untagged)]
/// Colour representation
pub enum Colour { pub enum Colour {
/// Colour represented as RGB /// Colour represented as RGB
Rgb(Rgb), Rgb(Rgb),
@@ -53,22 +56,22 @@ impl From<Colour> for Color32 {
} }
} }
/// Colour represented as a Hex string
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct Hex(pub HexColor); pub struct Hex(HexColor);
#[cfg(feature = "schemars")] #[cfg(feature = "schemars")]
impl schemars::JsonSchema for Hex { impl schemars::JsonSchema for Hex {
fn schema_name() -> std::borrow::Cow<'static, str> { fn schema_name() -> String {
std::borrow::Cow::Borrowed("Hex") String::from("Hex")
} }
fn json_schema(_: &mut SchemaGenerator) -> Schema { fn json_schema(_: &mut SchemaGenerator) -> Schema {
schemars::json_schema!({ SchemaObject {
"type": "string", instance_type: Some(InstanceType::String.into()),
"format": "color-hex", format: Some("color-hex".to_string()),
"description": "Colour represented as a Hex string" ..Default::default()
}) }
.into()
} }
} }
@@ -83,7 +86,6 @@ impl From<Colour> for u32 {
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Colour represented as RGB
pub struct Rgb { pub struct Rgb {
/// Red /// Red
pub r: u32, pub r: u32,
+8 -8
View File
@@ -6,17 +6,17 @@
use std::ffi::c_void; use std::ffi::c_void;
use std::ops::Deref; 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::HWND;
use windows::Win32::Foundation::RECT; use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::SIZE; use windows::Win32::Foundation::SIZE;
use windows::Win32::UI::Shell::Common::IObjectArray; 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; use windows_core::BOOL;
type DesktopID = GUID; 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 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 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) pub unsafe fn is_equal_by_app_user_model_id(&self, id: PCWSTR, out_result: *mut INT)
-> HRESULT; -> HRESULT;
/*** IApplicationView methods ***/ /*** IApplicationView methods ***/
pub unsafe fn get_view_state(&self, out_state: *mut UINT) -> HRESULT; // Proc20 pub unsafe fn get_view_state(&self, out_state: *mut UINT) -> HRESULT; // Proc20

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