Compare commits

..

1 Commits

Author SHA1 Message Date
LGUG2Z
7a6905b9e8 fix(wm): forcibly disable transparency on un/stack-all 2025-10-30 16:12:00 -07:00
109 changed files with 68255 additions and 26413 deletions

1
.envrc
View File

@@ -1 +0,0 @@
use flake

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
*.json text diff

View File

@@ -55,7 +55,6 @@ body:
label: Hotkey Configuration
description: >
Please provide your whkdrc or komorebi.ahk hotkey configuration file
render: shell
- type: textarea
validations:
required: true
@@ -63,4 +62,3 @@ body:
label: Output of komorebic check
description: >
Please provide the output of `komorebic check`
render: shell

View File

@@ -13,15 +13,15 @@ on:
- hotfix/*
tags:
- v*
schedule:
- cron: "30 0 * * 0" # Every day at 00:30 UTC
# schedule:
# - cron: "30 0 * * 0" # Every day at 00:30 UTC
workflow_dispatch:
jobs:
cargo-deny:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: EmbarkStudios/cargo-deny-action@v2
@@ -43,7 +43,7 @@ jobs:
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
with:
fetch-depth: 0
- run: rustup toolchain install stable --profile minimal
@@ -65,7 +65,7 @@ jobs:
- run: |
cargo install cargo-wix
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v5
with:
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
path: |
@@ -82,12 +82,12 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
with:
fetch-depth: 0
- shell: bash
run: echo "VERSION=nightly" >> $GITHUB_ENV
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v6
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
@@ -129,14 +129,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v6
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
@@ -171,14 +171,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.ref_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v6
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi

7
.gitignore vendored
View File

@@ -6,10 +6,3 @@ dummy.go
komorebic/applications.yaml
komorebic/applications.json
/.vs
/bar-schema
/komorebi-schema
/.wrangler
/.xwin-cache
result
/.direnv
procdump.exe

2816
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,15 +2,14 @@
resolver = "2"
members = [
"komorebi",
"komorebi-client",
"komorebi-gui",
"komorebi-layouts",
"komorebic",
"komorebic-no-console",
"komorebi-bar",
"komorebi-themes",
"komorebi-shortcuts",
"komorebi",
"komorebi-client",
"komorebi-gui",
"komorebic",
"komorebic-no-console",
"komorebi-bar",
"komorebi-themes",
"komorebi-shortcuts",
]
[workspace.dependencies]
@@ -20,23 +19,23 @@ chrono = "0.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
color-eyre = "0.6"
eframe = "0.33"
egui_extras = "0.33"
eframe = "0.32"
egui_extras = "0.32"
dirs = "6"
dunce = "1"
hotwatch = "0.5"
schemars = "1.1"
schemars = "0.8"
lazy_static = "1"
serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" }
serde_yaml = "0.9"
strum = { version = "0.28", features = ["derive"] }
strum = { version = "0.27", features = ["derive"] }
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
parking_lot = "0.12"
paste = "1"
sysinfo = "0.38"
sysinfo = "0.37"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
windows-numerics = { version = "0.3" }
@@ -49,32 +48,32 @@ which = "8"
[workspace.dependencies.windows]
version = "0.62"
features = [
"Foundation_Numerics",
"Win32_Devices",
"Win32_Devices_Display",
"Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader",
"Win32_System_Power",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",
"Win32_UI_Accessibility",
"Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming",
"Media",
"Media_Control",
"Foundation_Numerics",
"Win32_Devices",
"Win32_Devices_Display",
"Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader",
"Win32_System_Power",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",
"Win32_UI_Accessibility",
"Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming",
"Media",
"Media_Control",
]
[profile.release-opt]
@@ -83,6 +82,3 @@ lto = true
panic = "abort"
codegen-units = 1
strip = true
[workspace.metadata.crane]
name = "komorebi-workspace"

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)
## 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
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
@@ -79,7 +49,7 @@ Please refer to the [documentation](https://lgug2z.github.io/komorebi) for instr
to [install](https://lgug2z.github.io/komorebi/installation.html) and
[configure](https://lgug2z.github.io/komorebi/example-configurations.html)
_komorebi_, [common workflows](https://lgug2z.github.io/komorebi/common-workflows/komorebi-config-home.html), a complete
[configuration schema reference](https://komorebi-starlight.lgug2z.workers.dev/reference/komorebi-windows/) and a
[configuration schema reference](https://komorebi.lgug2z.com/schema) and a
complete [CLI reference](https://lgug2z.github.io/komorebi/cli/quickstart.html).
## Community
@@ -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.
```rust
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi" }
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.38"}
use anyhow::Result;
use komorebi_client::Notification;

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())

View File

@@ -1,8 +1,8 @@
[graph]
targets = [
"x86_64-pc-windows-msvc",
"i686-pc-windows-msvc",
"aarch64-pc-windows-msvc",
"x86_64-pc-windows-msvc",
"i686-pc-windows-msvc",
"aarch64-pc-windows-msvc",
]
all-features = false
no-default-features = false
@@ -12,31 +12,31 @@ feature-depth = 1
[advisories]
ignore = [
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" },
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" }
]
[licenses]
allow = [
"0BSD",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"Artistic-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"CC0-1.0",
"ISC",
"MIT",
"MIT-0",
"MPL-2.0",
"OFL-1.1",
"Ubuntu-font-1.0",
"Unicode-3.0",
"Zlib",
"LicenseRef-Komorebi-2.0",
"0BSD",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"Artistic-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"CC0-1.0",
"ISC",
"MIT",
"MIT-0",
"MPL-2.0",
"OFL-1.1",
"Ubuntu-font-1.0",
"Unicode-3.0",
"Zlib",
"LicenseRef-Komorebi-2.0"
]
confidence-threshold = 0.8
@@ -50,11 +50,6 @@ crate = "komorebi-client"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-layouts"
expression = "LicenseRef-Komorebi-2.0"
license-files = []
[[licenses.clarify]]
crate = "komorebic"
expression = "LicenseRef-Komorebi-2.0"
@@ -100,11 +95,6 @@ crate = "base16-egui-themes"
expression = "MIT"
license-files = []
[[licenses.clarify]]
crate = "win32-display-data"
expression = "0BSD"
license-files = []
[bans]
multiple-versions = "allow"
wildcards = "allow"
@@ -117,12 +107,11 @@ unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = [
"https://github.com/LGUG2Z/base16-egui-themes",
"https://github.com/LGUG2Z/windows-icons",
"https://github.com/LGUG2Z/win32-display-data",
"https://github.com/LGUG2Z/flavours",
"https://github.com/LGUG2Z/base16_color_scheme",
"https://github.com/LGUG2Z/whkd",
"https://github.com/LGUG2Z/catppuccin-egui",
"https://github.com/amPerl/egui-phosphor",
"https://github.com/LGUG2Z/base16-egui-themes",
"https://github.com/LGUG2Z/windows-icons",
"https://github.com/LGUG2Z/win32-display-data",
"https://github.com/LGUG2Z/flavours",
"https://github.com/LGUG2Z/base16_color_scheme",
"https://github.com/LGUG2Z/whkd",
# "https://github.com/LGUG2Z/catppuccin-egui",
]

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

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
```

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
```

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
```

View File

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

View File

@@ -1,218 +0,0 @@
# System Tray
The System Tray widget brings native Windows system tray icons into
`komorebi-bar`. It intercepts tray icon data by creating a hidden window that
mimics the Windows taskbar, receiving the same broadcast messages that
applications send via `Shell_NotifyIcon`.
## Basic configuration
```json
{
"right_widgets": [
{
"Systray": {
"enable": true
}
}
]
}
```
## Hiding icons
The `hidden_icons` config field accepts a list of rules. Each rule can be either
a plain string or a structured object.
A **plain string** matches the exe name (case-insensitive). This is the original
format, so existing configs continue to work without changes:
```json
"hidden_icons": [
"SecurityHealthSystray.exe",
"PhoneExperienceHost.exe"
]
```
A **structured object** matches one or more icon properties. All specified fields
must match (AND logic). By default matching is exact and case-insensitive.
```json
"hidden_icons": [
{ "exe": "svchost.exe", "tooltip": "Some Specific App" },
{ "guid": "{7820AE73-23E3-4229-82C1-E41CB67D5B9C}" },
{ "tooltip": "App I want hidden" }
]
```
The two forms can be mixed freely:
```json
"hidden_icons": [
"PhoneExperienceHost.exe",
{ "exe": "svchost.exe", "tooltip": "Specific Notification" },
{ "guid": "{7820AE73-23E3-4229-82C1-E41CB67D5B9C}" }
]
```
Available fields for structured rules:
| Field | Description |
|-----------|----------------------------------------------------------|
| `exe` | Executable name (e.g. `"SecurityHealthSystray.exe"`) |
| `tooltip` | Tooltip text shown on hover |
| `guid` | Icon GUID — most stable identifier across app restarts |
### Matching strategies
Each field can be a plain string (exact case-insensitive match) or an object
with `value` and `matching_strategy` for advanced matching. This uses the same
`MatchingStrategy` as komorebi's window rules.
```json
"hidden_icons": [
{
"exe": "explorer.exe",
"tooltip": { "value": "Network", "matching_strategy": "StartsWith" }
}
]
```
The above hides explorer.exe icons whose tooltip starts with "Network", while
leaving other explorer.exe icons visible.
Available strategies:
| Strategy | Description |
|---------------------|---------------------------------------------------|
| `Equals` | Exact match (default when using a plain string) |
| `StartsWith` | Value starts with the given text |
| `EndsWith` | Value ends with the given text |
| `Contains` | Value contains the given text |
| `Regex` | Value matches a regular expression |
| `DoesNotEqual` | Value does not exactly equal the given text |
| `DoesNotStartWith` | Value does not start with the given text |
| `DoesNotEndWith` | Value does not end with the given text |
| `DoesNotContain` | Value does not contain the given text |
All strategies except `Regex` are case-insensitive. For case-insensitive regex,
include `(?i)` in the pattern.
Plain strings and strategy objects can be mixed across fields:
```json
{
"exe": "explorer.exe",
"tooltip": { "value": "notification", "matching_strategy": "Contains" }
}
```
Run komorebi-bar with `RUST_LOG=info` to see the exe, tooltip, and GUID of every
systray icon in the log output.
## Stale icon cleanup
Some applications (e.g. Docker Desktop) may exit without properly removing their
tray icon. The widget detects these stale icons by checking whether the owning
window still exists via the Win32 `IsWindow` API.
### Automatic cleanup
By default, the widget checks for stale icons every 60 seconds. The interval
can be configured with `stale_icons_check_interval` (in seconds). The value is
clamped between 30 and 600. Set to 0 to disable automatic cleanup.
```json
"stale_icons_check_interval": 120
```
### Refresh button
A manual refresh button can be shown by setting `refresh_button`. Clicking it
immediately removes any stale icons.
- `"Visible"` — shows the button in the main icon area
- `"Overflow"` — shows the button in the hidden/overflow section (appears when
the overflow toggle is expanded)
```json
"refresh_button": "Overflow"
```
When set to `"Overflow"`, the overflow toggle arrow will appear even if there are
no hidden icons, so the refresh button remains accessible.
## Info button
An info button can be shown to open a floating panel that lists all systray icons
with their exe name, tooltip, GUID, and visibility status. This is useful for
identifying which icons to filter with `hidden_icons` rules.
- `"Visible"` — shows the button in the main icon area
- `"Overflow"` — shows the button in the hidden/overflow section
```json
"info_button": "Visible"
```
The info panel shows **all** icons, including those hidden by rules or the OS.
Each row shows the icon image, exe name, tooltip, GUID, and whether it is visible.
Copy buttons are provided on the exe, tooltip, and GUID cells for easy copying
(e.g. to paste a GUID into a filter rule).
Like the refresh button, setting `info_button` to `"Overflow"` will make the
overflow toggle arrow appear even if there are no hidden icons.
## Shortcuts button
A button that toggles komorebi-shortcuts. If the shortcuts process is running
it will be killed; otherwise it will be started.
- `"Visible"` — shows the button in the main icon area
- `"Overflow"` — shows the button in the hidden/overflow section
```json
"shortcuts_button": "Visible"
```
Like the other buttons, setting `shortcuts_button` to `"Overflow"` will make the
overflow toggle arrow appear even if there are no hidden icons.
## Mouse interactions
The widget supports left-click, right-click, middle-click, and double-click on
tray icons. Double-click sends the `LeftDoubleClick` action (via systray-util
0.2.0), which delivers `WM_LBUTTONDBLCLK` and `NIN_SELECT` messages to the icon.
## Click fallbacks
Some systray icons register a click callback but never actually respond to click
messages, effectively becoming "zombie" icons from an interaction standpoint. For
known problematic icons, the widget overrides the native click action with a
direct shell command. Fallback commands take priority — if a fallback is defined
for an icon, it always runs regardless of whether the icon reports itself as
clickable.
| Exe | Tooltip condition | Fallback command |
|--------------------------------|-------------------|---------------------------------|
| `SecurityHealthSystray.exe` | any | `start windowsdefender://` |
| `explorer.exe` | ends with `%` | `start ms-settings:apps-volume` |
| `explorer.exe` | empty | `start ms-settings:batterysaver`|
## Full example
```json
{
"Systray": {
"enable": true,
"hidden_icons": [
"SecurityHealthSystray.exe",
{ "exe": "explorer.exe", "tooltip": { "value": "Network", "matching_strategy": "StartsWith" } }
],
"stale_icons_check_interval": 60,
"refresh_button": "Overflow",
"info_button": "Visible",
"shortcuts_button": "Overflow"
}
}
```

View File

@@ -1,40 +0,0 @@
# Komorebi Bar
`komorebi-bar` is a status bar for komorebi that renders on top of the tiling
window manager. It is configured through a `komorebi.bar.json` file, either
alongside your `komorebi.json` or at the path specified in the
`bar_configurations` array.
## Widgets
Widgets are placed in the `left_widgets`, `center_widgets`, or `right_widgets`
arrays. Each widget is an object with the widget type as key and its
configuration as value.
| Widget | Description |
|--------------|--------------------------------------------------------|
| `Komorebi` | Workspaces, layout, focused window, and more |
| `Battery` | Battery level and charging status |
| `Date` | Current date in configurable format |
| `Time` | Current time in configurable format |
| `Media` | Currently playing media information |
| `Memory` | System memory usage |
| `Network` | Network activity and connection status |
| `Storage` | Disk usage information |
| `Update` | Komorebi update notification |
| `Systray` | Windows system tray icons |
Widgets with dedicated documentation pages:
- [System Tray](bar-widgets/systray.md)
> Dedicated pages for the remaining widgets will be added in the future.
## Schema
The full configuration schema is available at
[komorebi-bar.lgug2z.com/schema](https://komorebi-bar.lgug2z.com/schema).
For running a bar on each monitor, see
[Multiple Bar Instances](multiple-bar-instances.md) and
[Multi-Monitor Setup](multi-monitor-setup.md).

View File

@@ -1,337 +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
## Layout Options Rules
You can dynamically change `layout_options` based on the number of containers on a workspace
using `layout_options_rules`. This uses the same threshold-based logic as `layout_rules`:
when the container count is greater than or equal to a threshold, the highest matching
threshold's options are used.
Rules **fully replace** the base `layout_options` when they match. If no rule matches, the
base `layout_options` is used.
### Configuration
```json
{
"monitors": [
{
"workspaces": [
{
"name": "main",
"layout": "VerticalStack",
"layout_options": {
"column_ratios": [0.6],
"row_ratios": [0.4]
},
"layout_options_rules": {
"3": { "column_ratios": [0.55] },
"5": { "column_ratios": [0.3, 0.3, 0.3], "row_ratios": [0.5] }
}
}
]
}
]
}
```
In the example above:
| Container Count | Effective `layout_options` |
|-----------------|---------------------------|
| 1-2 | Base: `column_ratios: [0.6]`, `row_ratios: [0.4]` |
| 3-4 | Rule "3": `column_ratios: [0.55]` (no row_ratios, no scrolling, no grid) |
| 5+ | Rule "5": `column_ratios: [0.3, 0.3, 0.3]`, `row_ratios: [0.5]` |
Rules can include any field that `layout_options` supports: `column_ratios`, `row_ratios`,
`scrolling`, and `grid`. When a rule matches, it completely replaces the base options. Fields
not specified in the matching rule default to their standard defaults (not the base
`layout_options` values).
### Example: Scrolling Layout with Dynamic Columns
```json
{
"layout": "Scrolling",
"layout_options": {
"scrolling": { "columns": 2 }
},
"layout_options_rules": {
"4": { "scrolling": { "columns": 3 } },
"7": { "scrolling": { "columns": 4 } }
}
}
```
This increases the visible scrolling columns as more windows are added.
## Layout Defaults
You can define global per-layout default `layout_options` and `layout_options_rules` using
the top-level `layout_defaults` setting. This avoids repeating the same configuration across
every workspace that uses the same layout.
### Configuration
```json
{
"layout_defaults": {
"VerticalStack": {
"layout_options": { "column_ratios": [0.7] },
"layout_options_rules": {
"2": { "column_ratios": [0.7] },
"3": { "column_ratios": [0.55] },
"5": { "column_ratios": [0.4] }
}
},
"Columns": {
"layout_options": { "column_ratios": [0.3, 0.4] },
"layout_options_rules": {
"4": { "column_ratios": [0.2, 0.3, 0.3] }
}
},
"HorizontalStack": {
"layout_options": { "row_ratios": [0.6] }
}
},
"monitors": [
{
"workspaces": [
{
"name": "main",
"layout": "VerticalStack"
}
]
}
]
}
```
In this example, every workspace using `VerticalStack`, `Columns`, or `HorizontalStack`
automatically gets the global `layout_options` and `layout_options_rules` without needing
to specify them per-workspace. Note that `VerticalStack` only has 2 columns (main + stack),
so only a single `column_ratios` value is meaningful, while `Columns` distributes windows
across multiple columns where additional ratios control each column's width.
### Resolution Cascade
Global defaults act as a fallback. If a workspace defines **either** `layout_options` or
`layout_options_rules`, it **completely replaces** all global `layout_defaults` for that
layout. Global defaults are only used when the workspace has **neither** setting.
Within the effective source (workspace or global):
1. Try threshold match from the rules (highest matching threshold wins)
2. If a rule matches → use it (full replacement of base options)
3. Otherwise → use the base `layout_options`
### Override Examples
| Workspace Config | Global Config | Effective Behavior |
|------------------|---------------|--------------------|
| No `layout_options`, no rules | `layout_defaults` has both | Uses global base + global rules |
| Has `layout_options` only | `layout_defaults` has both | Workspace base only (all globals ignored) |
| Has `layout_options_rules` only | `layout_defaults` has both | Workspace rules only (all globals ignored) |
| Has both | `layout_defaults` has both | All workspace (all globals ignored) |
This "complete replacement" semantic means you never get a mix of workspace and global
settings for the same layout. If you override anything at the workspace level, you take
full control of that layout's options for that workspace.
## 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.

View File

@@ -83,7 +83,7 @@ is a crude hack trying to compensate for the insistence of Microsoft Windows
design teams to make custom borders with widths that are actually visible to
the user a thing of the past and removing this capability from the Win32 API.
I know it's buggy, and I know that most of the time it sucks, but this is something
I know it's buggy, and I know that most of the it sucks, but this is something
you should be bring up with the billion dollar company and not with me, the
solo developer.

View File

@@ -1,5 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.41/schema.bar.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.39/schema.bar.json",
"monitor": 0,
"font_family": "JetBrains Mono",
"theme": {
"palette": "Base16",
@@ -31,24 +32,24 @@
},
{
"Media": {
"enable": false
"enable": true
}
},
{
"Storage": {
"enable": false
"enable": true
}
},
{
"Memory": {
"enable": false
"enable": true
}
},
{
"Network": {
"enable": false,
"show_activity": true,
"show_total_activity": true
"enable": true,
"show_total_data_transmitted": true,
"show_network_activity": true
}
},
{

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.41/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.39/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",

179
flake.lock generated
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
flake.nix
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;
};
};
};
};
}

View File

@@ -75,38 +75,23 @@ trace target $RUST_LOG="trace":
deadlock $RUST_LOG="trace":
cargo +stable run --bin komorebi --locked --no-default-features --features deadlock_detection
docgen starlight:
rm {{ starlight }}/src/data/cli/windows/*.md
cargo run --package komorebic -- docgen --output {{ starlight }}/src/data/cli/windows
schemars-docgen ./schema.json --output {{ starlight }}/src/content/docs/reference/komorebi-windows.mdx --title "komorebi.json (Windows)" --description "komorebi for Windows configuration schema reference"
schemars-docgen ./schema.bar.json --output {{ starlight }}/src/content/docs/reference/bar-windows.mdx --title "komorebi.bar.json (Windows)" --description "komorebi-bar for Windows configuration schema reference"
docgen:
cargo run --package komorebic -- docgen
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
jsonschema:
cargo run --package komorebic -- static-config-schema > schema.json
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
cargo run --package komorebi-bar -- --schema > schema.bar.json
version := `cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "komorebi") | .version'`
schemapub:
rm -Force komorebi-schema
mkdir -Force komorebi-schema
cp schema.json komorebi-schema/komorebi.{{ version }}.schema.json
cp schema.bar.json komorebi-schema/komorebi.bar.{{ version }}.schema.json
npx wrangler pages deploy --project-name komorebi --branch main .\komorebi-schema
depcheck:
cargo outdated --depth 2
cargo +nightly udeps --quiet
deps:
cargo update
just depgen
# this part is run in a nix shell because python is a nightmare
schemagen:
rm -rf static-config-docs bar-config-docs
mkdir -p static-config-docs bar-config-docs
generate-schema-doc ./schema.json --config template_name=js_offline --config minify=false ./static-config-docs/
generate-schema-doc ./schema.bar.json --config template_name=js_offline --config minify=false ./bar-config-docs/
mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html
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

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-bar"
version = "0.1.42"
version = "0.1.39"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -17,18 +17,16 @@ crossbeam-channel = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
eframe = { workspace = true }
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" }
egui_extras = { workspace = true }
egui-phosphor = "0.10"
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
lazy_static = { workspace = true }
netdev = "0.41"
netdev = "0.36"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
parking_lot = { workspace = true }
regex = "1"
random_word = { version = "0.5", features = ["en"] }
reqwest = { version = "0.12", features = ["blocking"] }
schemars = { workspace = true, optional = true }
@@ -36,8 +34,6 @@ serde = { workspace = true }
serde_json = { workspace = true }
starship-battery = "0.10"
sysinfo = { workspace = true }
systray-util = "0.2.0"
tokio = { version = "1", features = ["rt", "sync", "time"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
which = { workspace = true }
@@ -48,8 +44,4 @@ windows-icons-fallback = { package = "windows-icons", git = "https://github.com/
[features]
default = ["schemars"]
schemars = [
"dep:schemars",
"komorebi-client/default",
"komorebi-themes/default",
]
schemars = ["dep:schemars", "komorebi-client/default", "komorebi-themes/default"]

View File

@@ -18,7 +18,6 @@ use crate::render::Color32Ext;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::take_widget_clicked;
use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::MonitorInfo;
use crate::widgets::widget::BarWidget;
@@ -56,9 +55,6 @@ 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 lazy_static::lazy_static;
use parking_lot::Mutex;
@@ -187,12 +183,12 @@ pub fn apply_theme(
render_config: Rc<RefCell<RenderConfig>>,
) {
let (auto_select_fill, auto_select_text) = match theme {
KomobarTheme::Catppuccin(KomobarThemeCatppuccin {
KomobarTheme::Catppuccin {
name: catppuccin,
accent: catppuccin_value,
auto_select_fill: catppuccin_auto_select_fill,
auto_select_text: catppuccin_auto_select_text,
}) => {
} => {
match catppuccin {
Catppuccin::Frappe => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
@@ -257,12 +253,12 @@ pub fn apply_theme(
catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())),
)
}
KomobarTheme::Base16(KomobarThemeBase16 {
KomobarTheme::Base16 {
name: base16,
accent: base16_value,
auto_select_fill: base16_auto_select_fill,
auto_select_text: base16_auto_select_text,
}) => {
} => {
ctx.set_style(base16.style());
let base16_value = base16_value.unwrap_or_default();
let accent = base16_value.color32(Base16Wrapper::Base16(base16));
@@ -280,12 +276,12 @@ pub fn apply_theme(
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))),
)
}
KomobarTheme::Custom(KomobarThemeCustom {
KomobarTheme::Custom {
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();
@@ -454,11 +450,10 @@ impl Komobar {
self.right_widgets = right_widgets;
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)
}
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None),
None => (0, None),
MonitorConfigOrIndex::Index(idx) => (*idx, None),
};
let mapped_info = self.monitor_info.as_ref().map(|info| {
@@ -868,13 +863,9 @@ impl eframe::App for Komobar {
Ok(KomorebiEvent::Notification(notification)) => {
let state = &notification.state;
let usr_monitor_index = match &self.config.monitor {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
monitor_config.index
}
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
None => 0,
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
MonitorConfigOrIndex::Index(idx) => *idx,
};
let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied();
self.monitor_index = monitor_index;
let mut should_apply_config = false;
@@ -1083,10 +1074,6 @@ impl eframe::App for Komobar {
let frame = render_config.change_frame_on_bar(frame, &ctx.style());
CentralPanel::default().frame(frame).show(ctx, |ui| {
// Variable to store command to execute after widgets are rendered
// This allows widgets to mark clicks as consumed before bar processes them
let mut pending_command: Option<crate::config::MouseMessage> = None;
if let Some(mouse_config) = &self.config.mouse {
let command = if ui
.input(|i| i.pointer.button_double_clicked(PointerButton::Primary))
@@ -1187,9 +1174,9 @@ impl eframe::App for Komobar {
&None
};
// Store the command to execute after widgets are rendered
// This allows widgets to mark clicks as consumed
pending_command = command.clone();
if let Some(command) = command {
command.execute(self.mouse_follows_focus);
}
}
// Apply grouping logic for the bar as a whole
@@ -1321,13 +1308,6 @@ impl eframe::App for Komobar {
});
});
}
// Execute the deferred mouse command only if no widget consumed the click
if let Some(command) = pending_command
&& !take_widget_clicked()
{
command.execute(self.mouse_follows_focus);
}
});
}
}

View File

@@ -5,6 +5,7 @@ use crate::widgets::widget::WidgetConfig;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
use eframe::egui::Vec2;
use komorebi_client::KomorebiTheme;
use komorebi_client::PathExt;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
@@ -15,10 +16,9 @@ use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// The `komorebi.bar.json` configuration file reference for `v0.1.42`
/// The `komorebi.bar.json` configuration file reference for `v0.1.39`
pub struct KomobarConfig {
/// Bar height
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50)))]
/// Bar height (default: 50)
pub height: Option<f32>,
/// Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or
/// vertical definition which can each take a single value for a symmetric padding or two
@@ -76,26 +76,20 @@ pub struct KomobarConfig {
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// The monitor index or the full monitor options
#[cfg_attr(feature = "schemars", schemars(extend("default" = MonitorConfigOrIndex::Index(0))))]
pub monitor: Option<MonitorConfigOrIndex>,
pub monitor: MonitorConfigOrIndex,
/// Font family
pub font_family: Option<String>,
/// Font size
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12.5)))]
/// Font size (default: 12.5)
pub font_size: Option<f32>,
/// Scale of the icons relative to the font_size [[1.0-2.0]]
#[cfg_attr(feature = "schemars", schemars(extend("default" = 1.4)))]
/// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
pub icon_scale: Option<f32>,
/// Max label width before text truncation
#[cfg_attr(feature = "schemars", schemars(extend("default" = 400.0)))]
/// Max label width before text truncation (default: 400.0)
pub max_label_width: Option<f32>,
/// Theme
pub theme: Option<KomobarTheme>,
/// Alpha value for the color transparency [[0-255]]
#[cfg_attr(feature = "schemars", schemars(extend("default" = 200)))]
/// Alpha value for the color transparency [[0-255]] (default: 200)
pub transparency_alpha: Option<u8>,
/// Spacing between widgets
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10.0)))]
/// Spacing between widgets (default: 10.0)
pub widget_spacing: Option<f32>,
/// Visual grouping for widgets
pub grouping: Option<Grouping>,
@@ -151,7 +145,6 @@ impl KomobarConfig {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Position configuration
pub struct PositionConfig {
/// The desired starting position of the bar (0,0 = top left of the screen)
#[serde(alias = "position")]
@@ -163,7 +156,6 @@ pub struct PositionConfig {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Frame configuration
pub struct FrameConfig {
/// Margin inside the painted frame
pub inner_margin: Position,
@@ -172,7 +164,6 @@ pub struct FrameConfig {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Monitor configuration or monitor index
pub enum MonitorConfigOrIndex {
/// The monitor index where you want the bar to show
Index(usize),
@@ -182,7 +173,6 @@ pub enum MonitorConfigOrIndex {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Monitor configuration
pub struct MonitorConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub index: usize,
@@ -200,13 +190,9 @@ pub type Margin = SpacingKind;
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
// `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the
// individual values.
/// Spacing kind
pub enum SpacingKind {
/// Spacing applied to all sides
All(f32),
/// Individual spacing applied to each side
Individual(IndividualSpacingConfig),
/// Grouped vertical and horizontal spacing
Grouped(GroupedSpacingConfig),
}
@@ -251,36 +237,25 @@ impl SpacingKind {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Grouped vertical and horizontal spacing
pub struct GroupedSpacingConfig {
/// Vertical grouped spacing
pub vertical: Option<GroupedSpacingOptions>,
/// Horizontal grouped spacing
pub horizontal: Option<GroupedSpacingOptions>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Grouped spacing options
pub enum GroupedSpacingOptions {
/// Symmetrical grouped spacing
Symmetrical(f32),
/// Split grouped spacing
Split(f32, f32),
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Individual spacing configuration
pub struct IndividualSpacingConfig {
/// Spacing for the top
pub top: f32,
/// Spacing for the bottom
pub bottom: f32,
/// Spacing for the left
pub left: f32,
/// Spacing for the right
pub right: f32,
}
@@ -360,7 +335,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:
@@ -405,13 +379,10 @@ pub enum MouseMessage {
#[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)))]
/// Send the FocusMonitorAtCursor message (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)))]
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message (default:true)
pub ignore_mouse_follows_focus: Option<bool>,
/// The message to send to the komorebi client
pub message: komorebi_client::SocketMessage,
@@ -419,7 +390,6 @@ pub struct KomorebiMouseMessage {
#[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>,
@@ -432,16 +402,14 @@ pub struct MouseConfig {
/// 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)))]
/// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
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)))]
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
pub horizontal_scroll_threshold: Option<f32>,
/// Command to send on scrolling left (every tick)
pub on_scroll_left: Option<MouseMessage>,
@@ -527,7 +495,6 @@ impl KomobarConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Position
pub struct Position {
/// X coordinate
pub x: f32,
@@ -553,11 +520,71 @@ impl From<Position> for Pos2 {
}
}
pub use komorebi_themes::KomobarTheme;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "palette")]
pub enum KomobarTheme {
/// A theme from catppuccin-egui
Catppuccin {
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
name: komorebi_themes::Catppuccin,
accent: Option<komorebi_themes::CatppuccinValue>,
auto_select_fill: Option<komorebi_themes::CatppuccinValue>,
auto_select_text: Option<komorebi_themes::CatppuccinValue>,
},
/// A theme from base16-egui-themes
Base16 {
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
name: komorebi_themes::Base16,
accent: Option<komorebi_themes::Base16Value>,
auto_select_fill: Option<komorebi_themes::Base16Value>,
auto_select_text: Option<komorebi_themes::Base16Value>,
},
/// A custom Base16 theme
Custom {
/// Colours of the custom Base16 theme palette
colours: Box<komorebi_themes::Base16ColourPalette>,
accent: Option<komorebi_themes::Base16Value>,
auto_select_fill: Option<komorebi_themes::Base16Value>,
auto_select_text: Option<komorebi_themes::Base16Value>,
},
}
impl From<KomorebiTheme> for KomobarTheme {
fn from(value: KomorebiTheme) -> Self {
match value {
KomorebiTheme::Catppuccin {
name, bar_accent, ..
} => Self::Catppuccin {
name,
accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
},
KomorebiTheme::Base16 {
name, bar_accent, ..
} => Self::Base16 {
name,
accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
},
KomorebiTheme::Custom {
colours,
bar_accent,
..
} => Self::Custom {
colours,
accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
},
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Label prefix
pub enum LabelPrefix {
/// Show no prefix
None,
@@ -571,7 +598,6 @@ pub enum LabelPrefix {
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Display format
pub enum DisplayFormat {
/// Show only icon
Icon,
@@ -586,10 +612,9 @@ pub enum DisplayFormat {
}
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)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
$(#[$type_meta])*
pub enum $new_enum {
// Add new variants
$(
@@ -610,9 +635,7 @@ macro_rules! extend_enum {
};
}
extend_enum!(
/// Workspaces display format
DisplayFormat, WorkspacesDisplayFormat, {
extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
/// Show all icons only
AllIcons,
/// Show both all icons and text
@@ -621,26 +644,6 @@ extend_enum!(
AllIconsAndTextOnSelected,
});
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Media widget display format
pub enum MediaDisplayFormat {
/// Show only the media info icon
Icon,
/// Show only the media info text (artist - title)
Text,
/// Show both icon and text
IconAndText,
/// Show only the control buttons (previous, play/pause, next)
ControlsOnly,
/// Show icon with control buttons
IconAndControls,
/// Show text with control buttons
TextAndControls,
/// Show icon, text, and control buttons
Full,
}
#[cfg(test)]
mod tests {
use serde::Deserialize;

View File

@@ -38,8 +38,6 @@ use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
use windows_core::BOOL;
use std::sync::atomic::AtomicBool;
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
@@ -48,20 +46,6 @@ pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
pub static DEFAULT_PADDING: f32 = 10.0;
/// Flag to indicate that a widget has consumed a click event this frame.
/// This prevents the bar's global mouse handler from also processing the click.
pub static WIDGET_CLICKED: AtomicBool = AtomicBool::new(false);
/// Mark that a widget has consumed a click event this frame.
pub fn mark_widget_clicked() {
WIDGET_CLICKED.store(true, Ordering::SeqCst);
}
/// Check if a widget has consumed a click event this frame and reset the flag.
pub fn take_widget_clicked() -> bool {
WIDGET_CLICKED.swap(false, Ordering::SeqCst)
}
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
@@ -130,8 +114,15 @@ fn main() -> color_eyre::Result<()> {
#[cfg(feature = "schemars")]
if opts.schema {
let bar_config = schemars::schema_for!(KomobarConfig);
let schema = serde_json::to_string_pretty(&bar_config)?;
let settings = schemars::r#gen::SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let generator = settings.into_generator();
let socket_message = generator.into_root_schema_for::<KomobarConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
println!("{schema}");
std::process::exit(0);
@@ -232,13 +223,11 @@ fn main() -> color_eyre::Result<()> {
)?)?;
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)
}
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None),
None => (0, None),
MonitorConfigOrIndex::Index(idx) => (*idx, None),
};
let monitor_index = state
.monitor_usr_idx_map
.get(&usr_monitor_index)

View File

@@ -27,24 +27,18 @@ static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "kind")]
/// Grouping
pub enum Grouping {
/// No grouping is applied
#[cfg_attr(feature = "schemars", schemars(title = "None"))]
None,
/// Widgets are grouped as a whole
#[cfg_attr(feature = "schemars", schemars(title = "Bar"))]
Bar(GroupingConfig),
/// Widgets are grouped by alignment
#[cfg_attr(feature = "schemars", schemars(title = "Alignment"))]
Alignment(GroupingConfig),
/// Widgets are grouped individually
#[cfg_attr(feature = "schemars", schemars(title = "Widget"))]
Widget(GroupingConfig),
}
#[derive(Clone)]
/// Render configuration
pub struct RenderConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub monitor_idx: usize,
@@ -99,9 +93,8 @@ impl RenderExt for &KomobarConfig {
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
let monitor_idx = match &self.monitor {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => monitor_config.index,
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
None => 0,
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
MonitorConfigOrIndex::Index(idx) => *idx,
};
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
@@ -363,7 +356,6 @@ impl RenderConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Grouping configuration
pub struct GroupingConfig {
/// Styles for the grouping
pub style: Option<GroupingStyle>,
@@ -375,9 +367,7 @@ pub struct GroupingConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Grouping Style
pub enum GroupingStyle {
/// Default
#[serde(alias = "CtByte")]
Default,
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
@@ -399,9 +389,8 @@ pub enum GroupingStyle {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Rounding configuration
pub enum RoundingConfig {
/// All 4 corners are the same
/// All 4 corners are the same
Same(f32),
/// All 4 corners are custom. Order: NW, NE, SW, SE
Individual([f32; 4]),

View File

@@ -34,7 +34,6 @@ 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,
@@ -45,14 +44,13 @@ pub struct ApplicationsConfig {
pub spacing: Option<f32>,
/// Default display format for all applications (optional).
/// Could be overridden per application. Defaults to `Icon`.
pub display: Option<ApplicationsDisplayFormat>,
pub display: Option<DisplayFormat>,
/// 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.
@@ -69,13 +67,12 @@ pub struct AppConfig {
/// 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>,
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Applications widget display format
pub enum ApplicationsDisplayFormat {
pub enum DisplayFormat {
/// Show only the application icon.
#[default]
Icon,
@@ -171,7 +168,7 @@ pub struct App {
/// Command to execute when the application is launched.
pub command: UserCommand,
/// Display format (icon, text, or both).
pub display: ApplicationsDisplayFormat,
pub display: DisplayFormat,
/// Whether to show the launch command on hover.
pub show_command_on_hover: bool,
}
@@ -186,9 +183,9 @@ impl App {
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 => {
DisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
DisplayFormat::Text => self.draw_name(ui),
DisplayFormat::IconAndText => {
self.draw_icon(ctx, ui, icon_config);
self.draw_name(ui);
}

View File

@@ -19,14 +19,12 @@ use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Battery widget configuration
pub struct BatteryConfig {
/// Enable the Battery widget
pub enable: bool,
/// Hide the widget if the battery is at full charge
pub hide_on_full_charge: Option<bool>,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,

View File

@@ -18,12 +18,10 @@ use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// CPU widget configuration
pub struct CpuConfig {
/// Enable the Cpu widget
pub enable: bool,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,

View File

@@ -62,7 +62,6 @@ impl CustomModifiers {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Date widget configuration
pub struct DateConfig {
/// Enable the Date widget
pub enable: bool,
@@ -105,7 +104,6 @@ impl From<DateConfig> for Date {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Date widget format
pub enum DateFormat {
/// Month/Date/Year format (09/08/24)
MonthDateYear,
@@ -116,10 +114,8 @@ pub enum DateFormat {
/// Day Date Month Year format (8 September 2024)
DayDateMonthYear,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
Custom(String),
/// Custom format with modifiers
#[cfg_attr(feature = "schemars", schemars(title = "CustomModifiers"))]
CustomModifiers(CustomModifiers),
}

View File

@@ -21,16 +21,15 @@ use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
const ERROR_TEXT: &str = "Error";
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Keyboard widget configuration
pub struct KeyboardConfig {
/// Enable the Input widget
pub enable: bool,
/// Data refresh interval
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
/// Data refresh interval (default: 1 second)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
@@ -38,7 +37,9 @@ pub struct KeyboardConfig {
impl From<KeyboardConfig> for Keyboard {
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 {
enable: value.enable,

View File

@@ -49,7 +49,6 @@ use std::sync::atomic::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget configuration
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: Option<KomorebiWorkspacesConfig>,
@@ -68,7 +67,6 @@ pub struct KomorebiConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget workspaces configuration
pub struct KomorebiWorkspacesConfig {
/// Enable the Komorebi Workspaces widget
pub enable: bool,
@@ -80,7 +78,6 @@ pub struct KomorebiWorkspacesConfig {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget layout configuration
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
@@ -92,7 +89,6 @@ pub struct KomorebiLayoutConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget workspace layer configuration
pub struct KomorebiWorkspaceLayerConfig {
/// Enable the Komorebi Workspace Layer widget
pub enable: bool,
@@ -104,12 +100,10 @@ pub struct KomorebiWorkspaceLayerConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget focused container configuration
pub struct KomorebiFocusedContainerConfig {
/// Enable the Komorebi Focused Container widget
pub enable: bool,
/// DEPRECATED: use `display` instead (Show the icon of the currently focused container)
#[deprecated(note = "Use `display` instead")]
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused container)
pub show_icon: Option<bool>,
/// Display format of the currently focused container
pub display: Option<DisplayFormat>,
@@ -117,7 +111,6 @@ pub struct KomorebiFocusedContainerConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget locked container configuration
pub struct KomorebiLockedContainerConfig {
/// Enable the Komorebi Locked Container widget
pub enable: bool,
@@ -129,7 +122,6 @@ pub struct KomorebiLockedContainerConfig {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget configuration switcher configuration
pub struct KomorebiConfigurationSwitcherConfig {
/// Enable the Komorebi Configurations widget
pub enable: bool,
@@ -514,9 +506,7 @@ impl FocusedContainerBar {
if !value.enable {
return None;
}
// Handle legacy setting - convert show_icon to display format
#[allow(deprecated)]
let format = value
.display
.unwrap_or(if value.show_icon.unwrap_or(false) {

View File

@@ -26,18 +26,11 @@ use std::fmt::Formatter;
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Komorebi layout kind
pub enum KomorebiLayout {
/// Predefined layout
#[cfg_attr(feature = "schemars", schemars(title = "Default"))]
Default(komorebi_client::DefaultLayout),
/// Monocle mode
Monocle,
/// Floating layer
Floating,
/// Paused
Paused,
/// Custom layout
Custom,
}

View File

@@ -1,6 +1,4 @@
use crate::MAX_LABEL_WIDTH;
use crate::bar::Alignment;
use crate::config::MediaDisplayFormat;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
@@ -16,39 +14,30 @@ use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Media widget configuration
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,
/// Display format of the media widget (defaults to IconAndText)
pub display: Option<MediaDisplayFormat>,
}
impl From<MediaConfig> for Media {
fn from(value: MediaConfig) -> Self {
Self::new(
value.enable,
value.display.unwrap_or(MediaDisplayFormat::IconAndText),
)
Self::new(value.enable)
}
}
#[derive(Clone, Debug)]
pub struct Media {
pub enable: bool,
pub display: MediaDisplayFormat,
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
}
impl Media {
pub fn new(enable: bool, display: MediaDisplayFormat) -> Self {
pub fn new(enable: bool) -> Self {
Self {
enable,
display,
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
.unwrap()
.join()
@@ -64,58 +53,6 @@ impl Media {
}
}
pub fn previous(&self) {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(op) = session.TrySkipPreviousAsync()
{
op.join().unwrap_or_default();
}
}
pub fn next(&self) {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(op) = session.TrySkipNextAsync()
{
op.join().unwrap_or_default();
}
}
fn is_playing(&self) -> bool {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(info) = session.GetPlaybackInfo()
&& let Ok(status) = info.PlaybackStatus()
{
return status == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing;
}
false
}
fn is_previous_enabled(&self) -> bool {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(info) = session.GetPlaybackInfo()
&& let Ok(controls) = info.Controls()
&& let Ok(enabled) = controls.IsPreviousEnabled()
{
return enabled;
}
false
}
fn is_next_enabled(&self) -> bool {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(info) = session.GetPlaybackInfo()
&& let Ok(controls) = info.Controls()
&& let Ok(enabled) = controls.IsNextEnabled()
{
return enabled;
}
false
}
fn has_session(&self) -> bool {
self.session_manager.GetCurrentSession().is_ok()
}
fn output(&mut self) -> String {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
@@ -140,96 +77,28 @@ impl Media {
impl BarWidget for Media {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
// Don't render if there's no active media session
if !self.has_session() {
return;
}
let output = self.output();
let show_icon = matches!(
self.display,
MediaDisplayFormat::Icon
| MediaDisplayFormat::IconAndText
| MediaDisplayFormat::IconAndControls
| MediaDisplayFormat::Full
);
let show_text = matches!(
self.display,
MediaDisplayFormat::Text
| MediaDisplayFormat::IconAndText
| MediaDisplayFormat::TextAndControls
| MediaDisplayFormat::Full
);
let show_controls = matches!(
self.display,
MediaDisplayFormat::ControlsOnly
| MediaDisplayFormat::IconAndControls
| MediaDisplayFormat::TextAndControls
| MediaDisplayFormat::Full
);
// Don't render if there's no media info and we're not showing controls-only
if output.is_empty() && !show_controls {
return;
}
let icon_font_id = config.icon_font_id.clone();
let text_font_id = config.text_font_id.clone();
let icon_color = ctx.style().visuals.selection.stroke.color;
let text_color = ctx.style().visuals.text_color();
let mut layout_job = LayoutJob::default();
if show_icon {
layout_job = LayoutJob::simple(
if !output.is_empty() {
let mut layout_job = LayoutJob::simple(
egui_phosphor::regular::HEADPHONES.to_string(),
icon_font_id.clone(),
icon_color,
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
}
if show_text {
layout_job.append(
&output,
if show_icon { 10.0 } else { 0.0 },
10.0,
TextFormat {
font_id: text_font_id,
color: text_color,
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
}
let is_playing = self.is_playing();
let is_previous_enabled = self.is_previous_enabled();
let is_next_enabled = self.is_next_enabled();
let disabled_color = text_color.gamma_multiply(0.5);
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
let prev_color = if is_previous_enabled {
text_color
} else {
disabled_color
};
let next_color = if is_next_enabled {
text_color
} else {
disabled_color
};
let play_pause_icon = if is_playing {
egui_phosphor::regular::PAUSE
} else {
egui_phosphor::regular::PLAY
};
let show_label = |ui: &mut Ui| {
if (show_icon || show_text)
&& SelectableFrame::new(false)
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
@@ -239,95 +108,15 @@ impl BarWidget for Media {
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job.clone()).selectable(false).truncate(),
Label::new(layout_job).selectable(false).truncate(),
)
})
.on_hover_text(&output)
.clicked()
{
self.toggle();
}
};
let show_previous = |ui: &mut Ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(LayoutJob::simple(
egui_phosphor::regular::SKIP_BACK.to_string(),
icon_font_id.clone(),
prev_color,
100.0,
))
.selectable(false),
)
})
.clicked()
&& is_previous_enabled
{
self.previous();
}
};
let show_play_pause = |ui: &mut Ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(LayoutJob::simple(
play_pause_icon.to_string(),
icon_font_id.clone(),
text_color,
100.0,
))
.selectable(false),
)
})
.on_hover_text(&output)
.clicked()
{
self.toggle();
}
};
let show_next = |ui: &mut Ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(LayoutJob::simple(
egui_phosphor::regular::SKIP_FORWARD.to_string(),
icon_font_id.clone(),
next_color,
100.0,
))
.selectable(false),
)
})
.clicked()
&& is_next_enabled
{
self.next();
}
};
config.apply_on_widget(false, ui, |ui| {
if is_reversed {
// Right panel renders right-to-left, so reverse order
if show_controls {
show_next(ui);
show_play_pause(ui);
show_previous(ui);
{
self.toggle();
}
show_label(ui);
} else {
// Left/center panel renders left-to-right, normal order
show_label(ui);
if show_controls {
show_previous(ui);
show_play_pause(ui);
show_next(ui);
}
}
});
});
}
}
}
}

View File

@@ -18,12 +18,10 @@ use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Memory widget configuration
pub struct MemoryConfig {
/// Enable the Memory widget
pub enable: bool,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,

View File

@@ -20,8 +20,6 @@ pub mod media;
pub mod memory;
pub mod network;
pub mod storage;
#[cfg(target_os = "windows")]
pub mod systray;
pub mod time;
pub mod update;
pub mod widget;
@@ -94,16 +92,10 @@ impl IconsCache {
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
self.images.write().unwrap().insert(id, image);
}
/// Removes the cached image and texture for the given icon ID.
pub fn remove(&self, id: &ImageIconId) {
self.images.write().unwrap().remove(id);
self.textures.write().unwrap().1.remove(id);
}
}
#[inline]
pub(crate) fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
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())
@@ -164,8 +156,6 @@ pub enum ImageIconId {
Path(Arc<Path>),
/// Windows HWND handle.
Hwnd(isize),
/// System tray icon identifier.
SystrayIcon(String),
}
impl From<&Path> for ImageIconId {

View File

@@ -15,18 +15,12 @@ use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network widget configuration
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
@@ -41,8 +35,7 @@ pub struct NetworkConfig {
/// Characters to reserve for received and transmitted activity
#[serde(alias = "network_activity_fill_characters")]
pub activity_left_padding: Option<usize>,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
@@ -52,7 +45,6 @@ pub struct NetworkConfig {
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network select configuration
pub struct NetworkSelectConfig {
/// Select the total received data when it's over this value
pub total_received_over: Option<u64>,
@@ -67,34 +59,28 @@ pub struct NetworkSelectConfig {
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let default_refresh_interval = 10;
let data_refresh_interval = value
.data_refresh_interval
.unwrap_or(default_refresh_interval);
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
show_total_activity: value.show_total_activity,
show_activity: value.show_activity,
show_default_interface: value.show_default_interface.unwrap_or(true),
networks_network_activity: Arc::new(Mutex::new(Networks::new_with_refreshed_list())),
default_interface: Arc::new(Mutex::new(String::new())),
interface_generation: Arc::new(AtomicU64::new(0)),
networks_network_activity: Networks::new_with_refreshed_list(),
default_interface: String::new(),
default_refresh_interval,
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
auto_select: value.auto_select,
activity_left_padding: value.activity_left_padding.unwrap_or_default(),
last_update_request_default_interface: Instant::now()
last_updated_default_interface: Instant::now()
.checked_sub(Duration::from_secs(default_refresh_interval))
.unwrap(),
last_state_total_activity: Arc::new(Mutex::new(vec![])),
last_state_activity: Arc::new(Mutex::new(vec![])),
last_update_request_network_activity: Arc::new(Mutex::new(
Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
)),
activity_generation: Arc::new(AtomicU64::new(0)),
last_state_total_activity: vec![],
last_state_activity: vec![],
last_updated_network_activity: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
@@ -104,82 +90,63 @@ pub struct Network {
pub show_total_activity: bool,
pub show_activity: bool,
pub show_default_interface: bool,
networks_network_activity: Arc<Mutex<Networks>>,
networks_network_activity: Networks,
default_refresh_interval: u64,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
auto_select: Option<NetworkSelectConfig>,
default_interface: Arc<Mutex<String>>,
interface_generation: Arc<AtomicU64>,
last_update_request_default_interface: Instant,
activity_generation: Arc<AtomicU64>,
last_state_activity: Arc<Mutex<Vec<NetworkReading>>>,
last_state_total_activity: Arc<Mutex<Vec<NetworkReading>>>,
last_update_request_network_activity: Arc<Mutex<Instant>>,
default_interface: String,
last_updated_default_interface: Instant,
last_state_total_activity: Vec<NetworkReading>,
last_state_activity: Vec<NetworkReading>,
last_updated_network_activity: Instant,
activity_left_padding: usize,
}
impl Network {
fn update_default_interface_async(&mut self) {
let gen_ = self.interface_generation.fetch_add(1, Ordering::SeqCst) + 1;
let gen_arc = Arc::clone(&self.interface_generation);
let iface_arc = Arc::clone(&self.default_interface);
thread::spawn(move || {
if let Ok(interface) = netdev::get_default_interface()
&& let Some(friendly_name) = &interface.friendly_name
{
// Only update if this is the latest request
if gen_ == gen_arc.load(Ordering::SeqCst)
&& let Ok(mut iface) = iface_arc.lock()
{
*iface = friendly_name.clone();
}
}
});
}
fn default_interface(&mut self) -> String {
let current = self.default_interface.lock().unwrap().clone();
fn default_interface(&mut self) {
let now = Instant::now();
if now.duration_since(self.last_update_request_default_interface)
if now.duration_since(self.last_updated_default_interface)
> Duration::from_secs(self.default_refresh_interval)
{
self.last_update_request_default_interface = now;
self.update_default_interface_async();
}
if let Ok(interface) = netdev::get_default_interface()
&& let Some(friendly_name) = &interface.friendly_name
{
self.default_interface.clone_from(friendly_name);
}
current
self.last_updated_default_interface = now;
}
}
fn update_network_activity_async(&mut self) {
let gen_ = self.activity_generation.fetch_add(1, Ordering::SeqCst) + 1;
let gen_arc = Arc::clone(&self.activity_generation);
let activity_arc = Arc::clone(&self.last_state_activity);
let total_activity_arc = Arc::clone(&self.last_state_total_activity);
let data_refresh_interval = self.data_refresh_interval;
let show_activity = self.show_activity;
let show_total_activity = self.show_total_activity;
let networks_network_activity_arc = Arc::clone(&self.networks_network_activity);
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let mut activity = self.last_state_activity.clone();
let mut total_activity = self.last_state_total_activity.clone();
let now = Instant::now();
thread::spawn(move || {
let mut activity = Vec::new();
let mut total_activity = Vec::new();
if now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
activity.clear();
total_activity.clear();
if let Ok(interface) = netdev::get_default_interface()
&& let Some(friendly_name) = &interface.friendly_name
&& let Ok(mut networks) = networks_network_activity_arc.lock()
{
networks.refresh(true);
self.default_interface.clone_from(friendly_name);
for (interface_name, data) in &*networks {
self.networks_network_activity.refresh(true);
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
if show_activity {
if self.show_activity {
let received =
Network::to_pretty_bytes(data.received(), data_refresh_interval);
let transmitted =
Network::to_pretty_bytes(data.transmitted(), data_refresh_interval);
Self::to_pretty_bytes(data.received(), self.data_refresh_interval);
let transmitted = Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
);
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
@@ -188,55 +155,26 @@ impl Network {
));
}
if show_total_activity {
let total_received = Network::to_pretty_bytes(data.total_received(), 1);
if self.show_total_activity {
let total_received = Self::to_pretty_bytes(data.total_received(), 1);
let total_transmitted =
Network::to_pretty_bytes(data.total_transmitted(), 1);
Self::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
if gen_ == gen_arc.load(Ordering::SeqCst) {
if let Ok(mut act) = activity_arc.lock() {
*act = activity;
}
if let Ok(mut tot) = total_activity_arc.lock() {
*tot = total_activity;
}
}
});
}
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let now = Instant::now();
let should_update = {
let last_update_request = self.last_update_request_network_activity.lock().unwrap();
now.duration_since(*last_update_request)
> Duration::from_secs(self.data_refresh_interval)
};
if should_update {
{
let mut last_updated = self.last_update_request_network_activity.lock().unwrap();
*last_updated = now;
}
self.update_network_activity_async();
self.last_state_activity.clone_from(&activity);
self.last_state_total_activity.clone_from(&total_activity);
self.last_updated_network_activity = now;
}
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)
}
@@ -507,9 +445,9 @@ impl BarWidget for Network {
}
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(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -523,11 +461,11 @@ impl BarWidget for Network {
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self_default_interface.insert_str(0, "NET: ");
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self_default_interface,
&self.default_interface,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),

View File

@@ -18,23 +18,17 @@ use sysinfo::Disks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Storage widget configuration
pub struct StorageConfig {
/// Enable the Storage widget
pub enable: bool,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Show disks that are read only
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
/// Show disks that are read only. (default: false)
pub show_read_only_disks: Option<bool>,
/// Show removable disks
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
/// Show removable disks. (default: true)
pub show_removable_disks: Option<bool>,
/// Storage display name
pub storage_display_name: Option<StorageDisplayName>,
/// 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]]
@@ -50,9 +44,6 @@ impl From<StorageConfig> for Storage {
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),
storage_display_name: value
.storage_display_name
.unwrap_or(StorageDisplayName::Mount),
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(),
@@ -60,19 +51,6 @@ impl From<StorageConfig> for Storage {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum StorageDisplayName {
/// Display label as mount point eg. C:\
Mount,
/// Display label as name eg. Local Disk
Name,
/// Display label as mount then name eg. C:\ Local Disk
MountAndName,
/// Display label as name then mount eg. Local Disk C:\
NameAndMount,
}
struct StorageDisk {
label: String,
selected: bool,
@@ -85,7 +63,6 @@ pub struct Storage {
label_prefix: LabelPrefix,
show_read_only_disks: bool,
show_removable_disks: bool,
storage_display_name: StorageDisplayName,
auto_select_over: Option<u8>,
auto_hide_under: Option<u8>,
last_updated: Instant,
@@ -109,17 +86,6 @@ impl Storage {
continue;
}
let mount = disk.mount_point();
let name = disk.name();
let display_name = match self.storage_display_name {
StorageDisplayName::Mount => mount.to_string_lossy(),
StorageDisplayName::Name => name.to_string_lossy(),
StorageDisplayName::MountAndName => {
mount.to_string_lossy() + name.to_string_lossy()
}
StorageDisplayName::NameAndMount => {
name.to_string_lossy() + mount.to_string_lossy()
}
};
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
@@ -133,7 +99,7 @@ impl Storage {
disks.push(StorageDisk {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", display_name, percentage)
format!("{} {}%", mount.to_string_lossy(), percentage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
},

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,6 @@ lazy_static! {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Time widget configuration
pub struct TimeConfig {
/// Enable the Time widget
pub enable: bool,
@@ -93,7 +92,7 @@ pub struct TimeConfig {
///}
/// ```
pub timezone: Option<String>,
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00
/// 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>,
}
@@ -120,7 +119,6 @@ impl From<TimeConfig> for Time {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Time format
pub enum TimeFormat {
/// Twelve-hour format (with seconds)
TwelveHour,
@@ -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)
BinaryRectangle,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
Custom(String),
}

View File

@@ -16,12 +16,10 @@ use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Update widget configuration
pub struct UpdateConfig {
/// Enable the Update widget
pub enable: bool,
/// Data refresh interval in hours
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12)))]
/// Data refresh interval (default: 12 hours)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,

View File

@@ -19,10 +19,6 @@ use crate::widgets::network::Network;
use crate::widgets::network::NetworkConfig;
use crate::widgets::storage::Storage;
use crate::widgets::storage::StorageConfig;
#[cfg(target_os = "windows")]
use crate::widgets::systray::Systray;
#[cfg(target_os = "windows")]
use crate::widgets::systray::SystrayConfig;
use crate::widgets::time::Time;
use crate::widgets::time::TimeConfig;
use crate::widgets::update::Update;
@@ -38,47 +34,18 @@ pub trait BarWidget {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Widget configuration
pub enum WidgetConfig {
/// Applications widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Applications"))]
Applications(ApplicationsConfig),
/// Battery widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Battery"))]
Battery(BatteryConfig),
/// CPU widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Cpu"))]
Cpu(CpuConfig),
/// Date widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Date"))]
Date(DateConfig),
/// Keyboard widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Keyboard"))]
Keyboard(KeyboardConfig),
/// Komorebi widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Komorebi"))]
Komorebi(KomorebiConfig),
/// Media widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Media"))]
Media(MediaConfig),
/// Memory widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Memory"))]
Memory(MemoryConfig),
/// Network widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Network"))]
Network(NetworkConfig),
/// Storage widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
Storage(StorageConfig),
/// System Tray widget configuration (Windows only)
#[cfg(target_os = "windows")]
#[cfg_attr(feature = "schemars", schemars(title = "Systray"))]
Systray(SystrayConfig),
/// Time widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
Time(TimeConfig),
/// Update widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Update"))]
Update(UpdateConfig),
}
@@ -95,8 +62,6 @@ impl WidgetConfig {
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
#[cfg(target_os = "windows")]
WidgetConfig::Systray(config) => Box::new(Systray::from(config)),
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
}
@@ -122,8 +87,6 @@ impl WidgetConfig {
WidgetConfig::Memory(config) => config.enable,
WidgetConfig::Network(config) => config.enable,
WidgetConfig::Storage(config) => config.enable,
#[cfg(target_os = "windows")]
WidgetConfig::Systray(config) => config.enable,
WidgetConfig::Time(config) => config.enable,
WidgetConfig::Update(config) => config.enable,
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-client"
version = "0.1.42"
version = "0.1.39"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -7,26 +7,19 @@ 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;

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-gui"
version = "0.1.42"
version = "0.1.39"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -13,4 +13,4 @@ egui_extras = { workspace = true }
random_word = { version = "0.5", features = ["en"] }
serde_json = { workspace = true }
windows-core = { workspace = true }
windows = { workspace = true }
windows = { workspace = true }

View File

@@ -247,7 +247,7 @@ impl eframe::App for KomorebiGui {
egui::CentralPanel::default().show(ctx, |ui| {
ctx.set_pixels_per_point(2.0);
egui::ScrollArea::vertical().show(ui, |ui| {
ui.set_width(ctx.content_rect().width());
ui.set_width(ctx.screen_rect().width());
ui.collapsing("Debugging", |ui| {
ui.collapsing("Window Rules", |ui| {
let window = Window::from(self.debug_hwnd);

View File

@@ -1,26 +0,0 @@
[package]
name = "komorebi-layouts"
version = "0.1.42"
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

View File

@@ -1,954 +0,0 @@
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 item in ratios.iter().take(MAX_RATIOS) {
assert_eq!(*item, 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 item in ratios.iter().take(MAX_RATIOS) {
assert_eq!(*item, 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() {
const {
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);
}
}
mod layout_options_rules_tests {
use super::*;
#[test]
fn test_hashmap_deserialization_ratios_only() {
// layout_options_rules entries with only ratios
// Note: ratios must sum to < 1.0 to avoid truncation by validate_ratios
let json = r#"{
"2": {"column_ratios": [0.7]},
"3": {"column_ratios": [0.55]},
"5": {"column_ratios": [0.3, 0.3, 0.3]}
}"#;
let rules: std::collections::HashMap<usize, LayoutOptions> =
serde_json::from_str(json).unwrap();
assert_eq!(rules.len(), 3);
assert_eq!(rules[&2].column_ratios.unwrap()[0], Some(0.7));
assert_eq!(rules[&3].column_ratios.unwrap()[0], Some(0.55));
let r5 = rules[&5].column_ratios.unwrap();
assert_eq!(r5[0], Some(0.3));
assert_eq!(r5[1], Some(0.3));
assert_eq!(r5[2], Some(0.3));
// No scrolling/grid in these entries
assert!(rules[&2].scrolling.is_none());
assert!(rules[&2].grid.is_none());
}
#[test]
fn test_hashmap_deserialization_full_options() {
// layout_options_rules entries with full options including scrolling/grid
let json = r#"{
"2": {"column_ratios": [0.7], "scrolling": {"columns": 3}},
"5": {"column_ratios": [0.3, 0.3, 0.3], "grid": {"rows": 2}}
}"#;
let rules: std::collections::HashMap<usize, LayoutOptions> =
serde_json::from_str(json).unwrap();
assert_eq!(rules.len(), 2);
assert_eq!(rules[&2].scrolling.unwrap().columns, 3);
assert!(rules[&2].grid.is_none());
assert!(rules[&5].scrolling.is_none());
assert_eq!(rules[&5].grid.unwrap().rows, 2);
}
#[test]
fn test_rule_entry_with_all_fields() {
let json = r#"{
"column_ratios": [0.6, 0.3],
"scrolling": {"columns": 4, "center_focused_column": true},
"grid": {"rows": 2},
"row_ratios": [0.5]
}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
let col = opts.column_ratios.unwrap();
assert_eq!(col[0], Some(0.6));
assert_eq!(col[1], Some(0.3));
let row = opts.row_ratios.unwrap();
assert_eq!(row[0], Some(0.5));
assert_eq!(opts.scrolling.unwrap().columns, 4);
assert_eq!(opts.scrolling.unwrap().center_focused_column, Some(true));
assert_eq!(opts.grid.unwrap().rows, 2);
}
#[test]
fn test_rule_entry_empty_object_gives_defaults() {
let json = r#"{}"#;
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
assert!(opts.column_ratios.is_none());
assert!(opts.row_ratios.is_none());
assert!(opts.scrolling.is_none());
assert!(opts.grid.is_none());
}
}
mod layout_default_entry_tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_default_layout_as_hashmap_key() {
let mut map: HashMap<DefaultLayout, &str> = HashMap::new();
map.insert(DefaultLayout::BSP, "bsp");
map.insert(DefaultLayout::VerticalStack, "vstack");
map.insert(DefaultLayout::Columns, "cols");
assert_eq!(map.len(), 3);
assert_eq!(map[&DefaultLayout::BSP], "bsp");
assert_eq!(map[&DefaultLayout::VerticalStack], "vstack");
assert_eq!(map[&DefaultLayout::Columns], "cols");
}
#[test]
fn test_default_layout_hash_consistency() {
// Same variant inserted twice should overwrite
let mut map: HashMap<DefaultLayout, i32> = HashMap::new();
map.insert(DefaultLayout::Grid, 1);
map.insert(DefaultLayout::Grid, 2);
assert_eq!(map.len(), 1);
assert_eq!(map[&DefaultLayout::Grid], 2);
}
#[test]
fn test_layout_default_entry_deserialize_full() {
let json = r#"{
"layout_options": {"column_ratios": [0.7]},
"layout_options_rules": {
"2": {"column_ratios": [0.7]},
"3": {"column_ratios": [0.55]},
"5": {"column_ratios": [0.3, 0.3, 0.3]}
}
}"#;
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
let base = entry.layout_options.unwrap();
assert_eq!(base.column_ratios.unwrap()[0], Some(0.7));
let rules = entry.layout_options_rules.unwrap();
assert_eq!(rules.len(), 3);
assert_eq!(rules[&2].column_ratios.unwrap()[0], Some(0.7));
assert_eq!(rules[&3].column_ratios.unwrap()[0], Some(0.55));
let r5 = rules[&5].column_ratios.unwrap();
assert_eq!(r5[0], Some(0.3));
assert_eq!(r5[1], Some(0.3));
assert_eq!(r5[2], Some(0.3));
}
#[test]
fn test_layout_default_entry_deserialize_only_base() {
let json = r#"{
"layout_options": {"column_ratios": [0.6]}
}"#;
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
assert!(entry.layout_options.is_some());
assert_eq!(
entry.layout_options.unwrap().column_ratios.unwrap()[0],
Some(0.6)
);
assert!(entry.layout_options_rules.is_none());
}
#[test]
fn test_layout_default_entry_deserialize_only_rules() {
let json = r#"{
"layout_options_rules": {
"3": {"column_ratios": [0.4]}
}
}"#;
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
assert!(entry.layout_options.is_none());
let rules = entry.layout_options_rules.unwrap();
assert_eq!(rules.len(), 1);
assert_eq!(rules[&3].column_ratios.unwrap()[0], Some(0.4));
}
#[test]
fn test_layout_default_entry_deserialize_empty() {
let json = r#"{}"#;
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
assert!(entry.layout_options.is_none());
assert!(entry.layout_options_rules.is_none());
}
#[test]
fn test_layout_default_entry_roundtrip() {
let json = r#"{
"layout_options": {"column_ratios": [0.7]},
"layout_options_rules": {
"2": {"column_ratios": [0.6]},
"5": {"column_ratios": [0.3, 0.3, 0.3]}
}
}"#;
let original: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
let serialized = serde_json::to_string(&original).unwrap();
let deserialized: LayoutDefaultEntry = serde_json::from_str(&serialized).unwrap();
assert_eq!(
original.layout_options.unwrap().column_ratios,
deserialized.layout_options.unwrap().column_ratios
);
let orig_rules = original.layout_options_rules.unwrap();
let deser_rules = deserialized.layout_options_rules.unwrap();
assert_eq!(orig_rules.len(), deser_rules.len());
for (key, orig_opts) in &orig_rules {
let deser_opts = &deser_rules[key];
assert_eq!(orig_opts.column_ratios, deser_opts.column_ratios);
}
}
#[test]
fn test_layout_defaults_full_config_deserialize() {
// Simulate the top-level layout_defaults field
let json = r#"{
"VerticalStack": {
"layout_options": {"column_ratios": [0.7]},
"layout_options_rules": {
"2": {"column_ratios": [0.7]},
"3": {"column_ratios": [0.55]}
}
},
"HorizontalStack": {
"layout_options": {"column_ratios": [0.6]}
},
"Columns": {
"layout_options_rules": {
"4": {"column_ratios": [0.3, 0.3, 0.3]}
}
}
}"#;
let defaults: HashMap<DefaultLayout, LayoutDefaultEntry> =
serde_json::from_str(json).unwrap();
assert_eq!(defaults.len(), 3);
// VerticalStack: has both base and rules
let vs = &defaults[&DefaultLayout::VerticalStack];
assert!(vs.layout_options.is_some());
assert_eq!(vs.layout_options_rules.as_ref().unwrap().len(), 2);
// HorizontalStack: has only base
let hs = &defaults[&DefaultLayout::HorizontalStack];
assert!(hs.layout_options.is_some());
assert!(hs.layout_options_rules.is_none());
// Columns: has only rules
let cols = &defaults[&DefaultLayout::Columns];
assert!(cols.layout_options.is_none());
assert_eq!(cols.layout_options_rules.as_ref().unwrap().len(), 1);
}
#[test]
fn test_layout_default_entry_with_scrolling_and_grid() {
let json = r#"{
"layout_options": {
"column_ratios": [0.5],
"scrolling": {"columns": 3},
"grid": {"rows": 2}
},
"layout_options_rules": {
"4": {
"scrolling": {"columns": 5, "center_focused_column": true}
}
}
}"#;
let entry: LayoutDefaultEntry = serde_json::from_str(json).unwrap();
let base = entry.layout_options.unwrap();
assert_eq!(base.scrolling.unwrap().columns, 3);
assert_eq!(base.grid.unwrap().rows, 2);
let rules = entry.layout_options_rules.unwrap();
let r4 = &rules[&4];
assert_eq!(r4.scrolling.unwrap().columns, 5);
assert_eq!(r4.scrolling.unwrap().center_focused_column, Some(true));
// Rule doesn't inherit base fields - full replacement
assert!(r4.column_ratios.is_none());
assert!(r4.grid.is_none());
}
#[test]
fn test_layout_default_entry_skip_serializing_none() {
// When both fields are None, they should not appear in output
let entry = LayoutDefaultEntry {
layout_options: None,
layout_options_rules: None,
};
let json = serde_json::to_string(&entry).unwrap();
assert!(!json.contains("layout_options"));
assert!(!json.contains("layout_options_rules"));
assert_eq!(json, "{}");
}
}
/// Tests for the complete-replacement cascade logic.
///
/// This mirrors the resolution algorithm in workspace.rs::update():
/// - If the workspace defines EITHER layout_options OR layout_options_rules,
/// it completely replaces the global layout_defaults for this layout.
/// - Global defaults are only used when the workspace has NEITHER setting.
/// - Within the effective source (workspace or global):
/// 1. Try threshold match from rules (highest matching threshold wins)
/// 2. If a rule matches -> use it (full replacement of base)
/// 3. Else -> use the base layout_options
///
/// Since the actual cascade is in workspace.rs (which has heavy WM dependencies),
/// we test the pure algorithm here using the same data structures.
mod cascade_resolution_tests {
use super::*;
/// Simulates the cascade resolution logic from workspace.rs::update().
/// This is a pure function equivalent of the inline code in update().
fn resolve_effective_options(
container_count: usize,
workspace_base: Option<LayoutOptions>,
workspace_rules: &[(usize, LayoutOptions)], // sorted by threshold ascending
global_base: Option<LayoutOptions>,
global_rules: &[(usize, LayoutOptions)], // sorted by threshold ascending
) -> Option<LayoutOptions> {
let has_workspace_overrides = workspace_base.is_some() || !workspace_rules.is_empty();
let (effective_base, effective_rules): (Option<LayoutOptions>, &[(usize, LayoutOptions)]) =
if has_workspace_overrides {
(workspace_base, workspace_rules)
} else {
(global_base, global_rules)
};
// Try threshold match from effective rules
let mut matched = None;
for (threshold, opts) in effective_rules {
if container_count >= *threshold {
matched = Some(*opts);
}
}
// If a rule matched, use it (full replacement); otherwise use effective base
if matched.is_some() {
matched
} else {
effective_base
}
}
fn opts_with_ratio(ratio: f32) -> LayoutOptions {
layout_options_with_column_ratios(&[ratio])
}
// --- No overrides ---
#[test]
fn test_no_workspace_no_global_returns_none() {
let result = resolve_effective_options(3, None, &[], None, &[]);
assert!(result.is_none());
}
// --- Base-only scenarios ---
#[test]
fn test_workspace_base_only() {
let ws_base = opts_with_ratio(0.7);
let result = resolve_effective_options(3, Some(ws_base), &[], None, &[]);
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
}
#[test]
fn test_global_base_only() {
let global_base = opts_with_ratio(0.6);
let result = resolve_effective_options(3, None, &[], Some(global_base), &[]);
assert_eq!(result.unwrap().column_ratios, global_base.column_ratios);
}
#[test]
fn test_workspace_base_overrides_all_globals() {
// Workspace has base → globals (both base and rules) are ignored entirely
let ws_base = opts_with_ratio(0.7);
let global_base = opts_with_ratio(0.6);
let global_rules = vec![(2, opts_with_ratio(0.5))];
let result =
resolve_effective_options(3, Some(ws_base), &[], Some(global_base), &global_rules);
// Workspace base wins; global rules are NOT used even though they would match
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
}
// --- Rules-only scenarios ---
#[test]
fn test_global_rules_match() {
let global_rules = vec![(2, opts_with_ratio(0.6)), (4, opts_with_ratio(0.5))];
// 3 containers: matches threshold 2, not 4
let result = resolve_effective_options(3, None, &[], None, &global_rules);
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.6));
}
#[test]
fn test_global_rules_highest_matching_threshold_wins() {
let global_rules = vec![(2, opts_with_ratio(0.6)), (4, opts_with_ratio(0.5))];
// 5 containers: matches both thresholds 2 and 4; highest (4) wins
let result = resolve_effective_options(5, None, &[], None, &global_rules);
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.5));
}
#[test]
fn test_global_rules_no_match_falls_through_to_none() {
let global_rules = vec![(5, opts_with_ratio(0.5))];
// 3 containers: doesn't match threshold 5
let result = resolve_effective_options(3, None, &[], None, &global_rules);
assert!(result.is_none());
}
#[test]
fn test_global_rules_no_match_falls_through_to_global_base() {
let global_base = opts_with_ratio(0.6);
let global_rules = vec![(5, opts_with_ratio(0.5))];
// 3 containers: doesn't match threshold 5, falls back to global base
let result = resolve_effective_options(3, None, &[], Some(global_base), &global_rules);
assert_eq!(result.unwrap().column_ratios, global_base.column_ratios);
}
#[test]
fn test_workspace_rules_override_global_rules() {
let ws_rules = vec![(2, opts_with_ratio(0.8))];
let global_rules = vec![(2, opts_with_ratio(0.6))];
// Workspace has rules → global rules are ignored entirely
let result = resolve_effective_options(3, None, &ws_rules, None, &global_rules);
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
}
// --- Complete replacement: workspace having EITHER setting disables ALL globals ---
#[test]
fn test_workspace_rules_disable_global_base() {
// Workspace has rules but no base. Global has base.
// Since workspace has a setting, globals are completely replaced.
let ws_rules = vec![(2, opts_with_ratio(0.8))];
let global_base = opts_with_ratio(0.6);
// Rule matches → use it. Global base is NOT available as fallback.
let result = resolve_effective_options(3, None, &ws_rules, Some(global_base), &[]);
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
}
#[test]
fn test_workspace_rules_no_match_does_not_fall_to_global_base() {
// Workspace has rules (but they don't match). Global has base.
// Since workspace has a setting, globals are completely replaced → returns None.
let ws_rules = vec![(5, opts_with_ratio(0.8))];
let global_base = opts_with_ratio(0.6);
let result = resolve_effective_options(3, None, &ws_rules, Some(global_base), &[]);
// No workspace base, no rule match, globals ignored → None
assert!(result.is_none());
}
#[test]
fn test_workspace_base_disables_global_rules() {
// Workspace has base but no rules. Global has rules.
// Since workspace has a setting, globals are completely replaced.
let ws_base = opts_with_ratio(0.7);
let global_rules = vec![(2, opts_with_ratio(0.5))];
// No workspace rules → no rule match → use workspace base. Global rules ignored.
let result = resolve_effective_options(3, Some(ws_base), &[], None, &global_rules);
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
}
#[test]
fn test_workspace_base_disables_global_rules_and_base() {
// Workspace has base. Global has both rules and base.
// Since workspace has a setting, all globals are completely replaced.
let ws_base = opts_with_ratio(0.7);
let global_base = opts_with_ratio(0.6);
let global_rules = vec![(2, opts_with_ratio(0.5))];
let result =
resolve_effective_options(3, Some(ws_base), &[], Some(global_base), &global_rules);
// Only workspace base is used; global rules and base are both ignored
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
}
#[test]
fn test_workspace_rules_disable_global_rules_and_base() {
// Workspace has rules. Global has both rules and base.
// Since workspace has a setting, all globals are completely replaced.
let ws_rules = vec![(2, opts_with_ratio(0.8))];
let global_base = opts_with_ratio(0.6);
let global_rules = vec![(2, opts_with_ratio(0.5))];
let result =
resolve_effective_options(3, None, &ws_rules, Some(global_base), &global_rules);
// Workspace rule matches → 0.8. Global base and rules both ignored.
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
}
// --- Full replacement semantics (rule match replaces base) ---
#[test]
fn test_rule_match_is_full_replacement_not_merge() {
// When a rule matches, its options FULLY REPLACE the base.
// Fields not specified in the rule default to their standard defaults.
let ws_base = layout_options_with_ratios(&[0.7], &[0.4]);
let rule_opts = layout_options_with_column_ratios(&[0.5]);
// rule_opts has column_ratios but no row_ratios
let ws_rules = vec![(2, rule_opts)];
let result = resolve_effective_options(3, Some(ws_base), &ws_rules, None, &[]);
let effective = result.unwrap();
// Column ratios come from the rule
assert_eq!(effective.column_ratios.unwrap()[0], Some(0.5));
// Row ratios are NOT inherited from ws_base - they're None (full replacement)
assert!(effective.row_ratios.is_none());
}
// --- Edge cases ---
#[test]
fn test_exact_threshold_match() {
let rules = vec![(3, opts_with_ratio(0.6))];
let result = resolve_effective_options(3, None, &rules, None, &[]);
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.6));
}
#[test]
fn test_container_count_one_below_threshold() {
let rules = vec![(3, opts_with_ratio(0.6))];
let result = resolve_effective_options(2, None, &rules, None, &[]);
assert!(result.is_none());
}
#[test]
fn test_zero_containers() {
let ws_base = opts_with_ratio(0.7);
let rules = vec![(1, opts_with_ratio(0.5))];
let result = resolve_effective_options(0, Some(ws_base), &rules, None, &[]);
// 0 containers doesn't match threshold 1 → falls back to workspace base
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
}
#[test]
fn test_many_thresholds_correct_match() {
let rules = vec![
(1, opts_with_ratio(0.8)),
(3, opts_with_ratio(0.6)),
(5, opts_with_ratio(0.4)),
(8, opts_with_ratio(0.3)),
];
// 6 containers: matches 1, 3, 5 but not 8. Highest match is 5.
let result = resolve_effective_options(6, None, &rules, None, &[]);
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.4));
}
#[test]
fn test_workspace_rules_disable_global_rules_even_if_ws_rules_dont_match() {
// Key behavior: if workspace has ANY setting, globals are entirely ignored.
// Even if workspace rules don't match, we don't fall back to global rules.
let ws_rules = vec![(10, opts_with_ratio(0.8))]; // threshold too high
let global_rules = vec![(2, opts_with_ratio(0.5))]; // would match
let result = resolve_effective_options(3, None, &ws_rules, None, &global_rules);
// Workspace has rules → all globals ignored. WS rules don't match → None.
assert!(result.is_none());
}
#[test]
fn test_all_four_sources_present_rules_match() {
// All four sources present: workspace base, workspace rules, global base, global rules
let ws_base = opts_with_ratio(0.7);
let ws_rules = vec![(2, opts_with_ratio(0.8))];
let global_base = opts_with_ratio(0.6);
let global_rules = vec![(2, opts_with_ratio(0.5))];
let result = resolve_effective_options(
3,
Some(ws_base),
&ws_rules,
Some(global_base),
&global_rules,
);
// Workspace has settings → uses workspace only. Rule matches → 0.8
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.8));
}
#[test]
fn test_all_four_sources_present_rules_no_match() {
// All four sources present, but workspace rules don't match
let ws_base = opts_with_ratio(0.7);
let ws_rules = vec![(10, opts_with_ratio(0.8))]; // threshold too high
let global_base = opts_with_ratio(0.6);
let global_rules = vec![(10, opts_with_ratio(0.5))]; // also too high
let result = resolve_effective_options(
3,
Some(ws_base),
&ws_rules,
Some(global_base),
&global_rules,
);
// Workspace has settings → uses workspace only. No rule match → workspace base 0.7
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
}
// --- Workspace with both base and rules ---
#[test]
fn test_workspace_both_rule_matches() {
let ws_base = opts_with_ratio(0.7);
let ws_rules = vec![(2, opts_with_ratio(0.5))];
let result = resolve_effective_options(3, Some(ws_base), &ws_rules, None, &[]);
// Rule matches → use rule (full replacement), not ws_base
assert_eq!(result.unwrap().column_ratios.unwrap()[0], Some(0.5));
}
#[test]
fn test_workspace_both_rule_no_match() {
let ws_base = opts_with_ratio(0.7);
let ws_rules = vec![(10, opts_with_ratio(0.5))];
let result = resolve_effective_options(3, Some(ws_base), &ws_rules, None, &[]);
// Rule doesn't match → fall back to ws_base
assert_eq!(result.unwrap().column_ratios, ws_base.column_ratios);
}
}

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::*;

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
}
}
}
}
}

View File

@@ -4,8 +4,8 @@ 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" }
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.9" }
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.9" }
eframe = { workspace = true }
dirs = { workspace = true }
dirs = { workspace = true }

View File

@@ -1,21 +1,18 @@
[package]
name = "komorebi-themes"
version = "0.1.42"
version = "0.1.39"
edition = "2024"
[dependencies]
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "3f157904c641f0dc80f043449fe0214fc4182425" }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a", default-features = false, features = [
"egui33",
] }
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "c9008bd5cfa288c926e9ea3aa18c92073f9281bd" }
catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
eframe = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_variant = "0.1"
strum = { workspace = true }
hex_color = { version = "3", features = ["serde"] }
flavours = { git = "https://github.com/LGUG2Z/flavours", rev = "24518c129918fe3260aa559eded7657e50752cb1" }
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
[features]
default = ["schemars"]

View File

@@ -1,8 +1,12 @@
use hex_color::HexColor;
#[cfg(feature = "schemars")]
use schemars::Schema;
#[cfg(feature = "schemars")]
use schemars::SchemaGenerator;
#[cfg(feature = "schemars")]
use schemars::schema::InstanceType;
#[cfg(feature = "schemars")]
use schemars::schema::Schema;
#[cfg(feature = "schemars")]
use schemars::schema::SchemaObject;
use crate::Color32;
use serde::Deserialize;
@@ -11,7 +15,6 @@ use serde::Serialize;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Colour representation
pub enum Colour {
/// Colour represented as RGB
Rgb(Rgb),
@@ -53,22 +56,22 @@ impl From<Colour> for Color32 {
}
}
/// Colour represented as a Hex string
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct Hex(pub HexColor);
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for Hex {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("Hex")
fn schema_name() -> String {
String::from("Hex")
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
schemars::json_schema!({
"type": "string",
"format": "color-hex",
"description": "Colour represented as a Hex string"
})
SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some("color-hex".to_string()),
..Default::default()
}
.into()
}
}
@@ -83,7 +86,6 @@ impl From<Colour> for u32 {
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Colour represented as RGB
pub struct Rgb {
/// Red
pub r: u32,

View File

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

View File

@@ -28,19 +28,18 @@ use serde_variant::to_variant_name;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "type")]
/// Theme
pub enum Theme {
/// Theme from catppuccin-egui
/// A theme from catppuccin-egui
Catppuccin {
name: Catppuccin,
accent: Option<CatppuccinValue>,
},
/// Theme from base16-egui-themes
/// A theme from base16-egui-themes
Base16 {
name: Base16,
accent: Option<Base16Value>,
},
/// Custom base16 palette
/// A custom base16 palette
Custom {
palette: Box<Base16ColourPalette>,
accent: Option<Base16Value>,
@@ -48,39 +47,22 @@ pub enum Theme {
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
/// Base16 colour palette: https://github.com/chriskempson/base16
pub struct Base16ColourPalette {
/// Base00
pub base_00: Colour,
/// Base01
pub base_01: Colour,
/// Base02
pub base_02: Colour,
/// Base03
pub base_03: Colour,
/// Base04
pub base_04: Colour,
/// Base05
pub base_05: Colour,
/// Base06
pub base_06: Colour,
/// Base07
pub base_07: Colour,
/// Base08
pub base_08: Colour,
/// Base09
pub base_09: Colour,
/// Base0A
pub base_0a: Colour,
/// Base0B
pub base_0b: Colour,
/// Base0C
pub base_0c: Colour,
/// Base0D
pub base_0d: Colour,
/// Base0E
pub base_0e: Colour,
/// Base0F
pub base_0f: Colour,
}
@@ -217,48 +199,28 @@ impl Theme {
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
/// Base16 value
pub enum Base16Value {
/// Base00
Base00,
/// Base01
Base01,
/// Base02
Base02,
/// Base03
Base03,
/// Base04
Base04,
/// Base05
Base05,
/// Base06
#[default]
Base06,
/// Base07
Base07,
/// Base08
Base08,
/// Base09
Base09,
/// Base0A
Base0A,
/// Base0B
Base0B,
/// Base0C
Base0C,
/// Base0D
Base0D,
/// Base0E
Base0E,
/// Base0F
Base0F,
}
/// Wrapper around a Base16 colour palette
pub enum Base16Wrapper {
/// Predefined Base16 colour palette
Base16(Base16),
/// Custom Base16 colour palette
Custom(Box<Base16ColourPalette>),
}
@@ -306,15 +268,10 @@ impl Base16Value {
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
/// Catppuccin palette
pub enum Catppuccin {
/// Frappe (https://catppuccin.com/palette#flavor-frappe)
Frappe,
/// Latte (https://catppuccin.com/palette#flavor-latte)
Latte,
/// Macchiato (https://catppuccin.com/palette#flavor-macchiato)
Macchiato,
/// Mocha (https://catppuccin.com/palette#flavor-mocha)
Mocha,
}
@@ -336,60 +293,33 @@ impl From<Catppuccin> for catppuccin_egui::Theme {
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
/// Catppuccin Value
pub enum CatppuccinValue {
/// Rosewater
Rosewater,
/// Flamingo
Flamingo,
/// Pink
Pink,
/// Mauve
Mauve,
/// Red
Red,
/// Maroon
Maroon,
/// Peach
Peach,
/// Yellow
Yellow,
/// Green
Green,
/// Teal
Teal,
/// Sky
Sky,
/// Sapphire
Sapphire,
/// Blue
Blue,
/// Lavender
Lavender,
#[default]
/// Text
Text,
/// Subtext1
Subtext1,
/// Subtext0
Subtext0,
/// Overlay2
Overlay2,
/// Overlay1
Overlay1,
/// Overlay0
Overlay0,
/// Surface2
Surface2,
/// Surface1
Surface1,
/// Surface0
Surface0,
/// Base
Base,
/// Mantle
Mantle,
/// Crust
Crust,
}
@@ -429,275 +359,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,
}),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.42"
version = "0.1.39"
description = "A tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2024"
@@ -8,10 +8,9 @@ edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-layouts = { path = "../komorebi-layouts", features = ["win32"] }
komorebi-themes = { path = "../komorebi-themes" }
base64 = "0.22"
base64 = "0.21"
bitflags = { version = "2", features = ["serde"] }
clap = { workspace = true }
chrono = { workspace = true }
@@ -51,8 +50,8 @@ windows-numerics = { workspace = true }
windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.56"
serde_with = { version = "3.19", features = ["schemars_1"] }
winreg = "0.55"
serde_with = { version = "3.12", features = ["schemars_0_8"] }
[build-dependencies]
shadow-rs = { workspace = true }
@@ -64,4 +63,4 @@ uuid = { version = "1", features = ["v4"] }
[features]
default = ["schemars"]
deadlock_detection = ["parking_lot/deadlock_detection"]
schemars = ["dep:schemars", "komorebi-layouts/schemars"]
schemars = ["dep:schemars"]

View File

@@ -86,7 +86,6 @@ impl AnimationEngine {
{
// cancel animation
ANIMATION_MANAGER.lock().cancel(animation_key.as_str());
render_dispatcher.cleanup_on_cancel();
return Ok(());
}

View File

@@ -1,363 +0,0 @@
use color_eyre::eyre;
use crossbeam_channel::Sender;
use crossbeam_channel::bounded;
use crossbeam_channel::unbounded;
use std::sync::OnceLock;
use std::time::Duration;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Dwm::DWM_THUMBNAIL_PROPERTIES;
use windows::Win32::Graphics::Dwm::DWM_TNP_OPACITY;
use windows::Win32::Graphics::Dwm::DWM_TNP_RECTDESTINATION;
use windows::Win32::Graphics::Dwm::DWM_TNP_SOURCECLIENTAREAONLY;
use windows::Win32::Graphics::Dwm::DWM_TNP_VISIBLE;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DestroyWindow;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::HWND_TOP;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PM_REMOVE;
use windows::Win32::UI::WindowsAndMessaging::PeekMessageW;
use windows::Win32::UI::WindowsAndMessaging::SET_WINDOW_POS_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SHOW_WINDOW_CMD;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOREDRAW;
use windows::Win32::UI::WindowsAndMessaging::SWP_NOZORDER;
use windows::Win32::UI::WindowsAndMessaging::SWP_SHOWWINDOW;
use windows::Win32::UI::WindowsAndMessaging::SetWindowPos;
use windows::Win32::UI::WindowsAndMessaging::ShowWindow;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::core::PCWSTR;
use crate::WindowsApi;
use crate::core::Rect;
use crate::windows_api;
const GHOST_CLASS_NAME: &[u16] = &[
b'k' as u16,
b'o' as u16,
b'm' as u16,
b'o' as u16,
b'r' as u16,
b'e' as u16,
b'b' as u16,
b'i' as u16,
b'-' as u16,
b'g' as u16,
b'h' as u16,
b'o' as u16,
b's' as u16,
b't' as u16,
0,
];
enum GhostCmd {
Create {
src_hwnd: isize,
start_rect: Rect,
z_above: Option<isize>,
reply: Sender<eyre::Result<(isize, isize)>>,
},
UpdateRect {
host_hwnd: isize,
hthumb: isize,
rect: Rect,
},
Destroy {
host_hwnd: isize,
hthumb: isize,
},
}
struct GhostOwner {
cmd_tx: Sender<GhostCmd>,
}
static GHOST_OWNER: OnceLock<GhostOwner> = OnceLock::new();
fn ghost_owner() -> &'static GhostOwner {
GHOST_OWNER.get_or_init(|| {
let (tx, rx) = unbounded::<GhostCmd>();
std::thread::Builder::new()
.name("komorebi-ghost-owner".into())
.spawn(move || run_owner_loop(rx))
.expect("failed to spawn ghost owner thread");
GhostOwner { cmd_tx: tx }
})
}
/// Eagerly initialise the ghost owner thread so the first movement animation
/// doesn't pay the spawn + class-registration cost. Idempotent. No-op for
/// users who never enable ghost movement only if it isn't called; calling
/// from a code path that's gated on `GHOST_MOVEMENT_ENABLED` keeps the lazy
/// guarantee.
pub fn prewarm() {
let _ = ghost_owner();
}
extern "system" fn ghost_wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
fn register_ghost_class() -> eyre::Result<()> {
let h_module = WindowsApi::module_handle_w()?;
let class_name = PCWSTR(GHOST_CLASS_NAME.as_ptr());
let window_class = WNDCLASSW {
hInstance: h_module.into(),
lpszClassName: class_name,
lpfnWndProc: Some(ghost_wnd_proc),
..Default::default()
};
// RegisterClassW returns 0 on failure with ERROR_CLASS_ALREADY_EXISTS as a
// benign error if the class is already registered. We tolerate that.
let _ = WindowsApi::register_class_w(&window_class);
Ok(())
}
fn run_owner_loop(cmd_rx: crossbeam_channel::Receiver<GhostCmd>) {
if let Err(error) = register_ghost_class() {
tracing::error!("ghost owner: failed to register class: {error}");
return;
}
loop {
// Drain any pending Win32 messages (DWM/system messages destined for our hosts).
unsafe {
let mut msg = MSG::default();
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
match cmd_rx.recv_timeout(Duration::from_millis(8)) {
Ok(cmd) => handle_cmd(cmd),
Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue,
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
}
}
}
fn handle_cmd(cmd: GhostCmd) {
match cmd {
GhostCmd::Create {
src_hwnd,
start_rect,
z_above,
reply,
} => {
let result = create_ghost(src_hwnd, start_rect, z_above);
let _ = reply.send(result);
}
GhostCmd::UpdateRect {
host_hwnd,
hthumb,
rect,
} => {
if let Err(error) = update_ghost(host_hwnd, hthumb, rect) {
tracing::trace!("ghost owner: update failed: {error}");
}
}
GhostCmd::Destroy { host_hwnd, hthumb } => {
destroy_ghost(host_hwnd, hthumb);
}
}
}
fn instance_handle() -> eyre::Result<isize> {
let h_module = WindowsApi::module_handle_w()?;
Ok(h_module.0 as isize)
}
fn create_ghost(
src_hwnd: isize,
start_rect: Rect,
z_above: Option<isize>,
) -> eyre::Result<(isize, isize)> {
let class_name = PCWSTR(GHOST_CLASS_NAME.as_ptr());
let host_hwnd = WindowsApi::create_ghost_host_window(class_name, instance_handle()?)?;
// Position the host at start_rect (Rect uses left/top + width/height).
let z_after = match z_above {
Some(hwnd) => HWND(windows_api::as_ptr!(hwnd)),
None => HWND_TOP,
};
let flags = SWP_NOACTIVATE | SWP_NOREDRAW | SWP_SHOWWINDOW;
unsafe {
let _ = SetWindowPos(
HWND(windows_api::as_ptr!(host_hwnd)),
Option::from(z_after),
start_rect.left,
start_rect.top,
start_rect.right,
start_rect.bottom,
flags,
);
}
let hthumb = match WindowsApi::dwm_register_thumbnail(host_hwnd, src_hwnd) {
Ok(h) => h,
Err(error) => {
unsafe {
let _ = DestroyWindow(HWND(windows_api::as_ptr!(host_hwnd)));
}
return Err(error);
}
};
let props = thumbnail_properties(start_rect.right, start_rect.bottom);
if let Err(error) = WindowsApi::dwm_update_thumbnail_properties(hthumb, &props) {
let _ = WindowsApi::dwm_unregister_thumbnail(hthumb);
unsafe {
let _ = DestroyWindow(HWND(windows_api::as_ptr!(host_hwnd)));
}
return Err(error);
}
// Make the host visible. Layered/transparent ext styles ensure no input.
unsafe {
let _ = ShowWindow(
HWND(windows_api::as_ptr!(host_hwnd)),
SHOW_WINDOW_CMD(8), // SW_SHOWNA
);
}
Ok((host_hwnd, hthumb))
}
fn update_ghost(host_hwnd: isize, hthumb: isize, rect: Rect) -> eyre::Result<()> {
let flags: SET_WINDOW_POS_FLAGS = SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOREDRAW;
unsafe {
SetWindowPos(
HWND(windows_api::as_ptr!(host_hwnd)),
None,
rect.left,
rect.top,
rect.right,
rect.bottom,
flags,
)?;
}
let props = thumbnail_properties(rect.right, rect.bottom);
WindowsApi::dwm_update_thumbnail_properties(hthumb, &props)
}
fn destroy_ghost(host_hwnd: isize, hthumb: isize) {
let _ = WindowsApi::dwm_unregister_thumbnail(hthumb);
unsafe {
let _ = DestroyWindow(HWND(windows_api::as_ptr!(host_hwnd)));
}
}
fn thumbnail_properties(width: i32, height: i32) -> DWM_THUMBNAIL_PROPERTIES {
DWM_THUMBNAIL_PROPERTIES {
dwFlags: DWM_TNP_VISIBLE
| DWM_TNP_RECTDESTINATION
| DWM_TNP_OPACITY
| DWM_TNP_SOURCECLIENTAREAONLY,
rcDestination: RECT {
left: 0,
top: 0,
right: width,
bottom: height,
},
rcSource: RECT::default(),
opacity: 255,
fVisible: true.into(),
fSourceClientAreaOnly: false.into(),
}
}
/// A live DWM-thumbnail "ghost" of a source window, used during movement
/// animations. While a ghost is active, the source window is typically cloaked
/// by the caller. The ghost is automatically disposed on drop, but callers
/// should prefer explicit `dispose()` to surface errors.
pub struct GhostWindow {
host_hwnd: isize,
hthumb: isize,
disposed: bool,
}
impl GhostWindow {
pub fn create(src_hwnd: isize, start_rect: Rect, z_above: Option<isize>) -> eyre::Result<Self> {
let (reply_tx, reply_rx) = bounded::<eyre::Result<(isize, isize)>>(1);
ghost_owner()
.cmd_tx
.send(GhostCmd::Create {
src_hwnd,
start_rect,
z_above,
reply: reply_tx,
})
.map_err(|e| eyre::eyre!("ghost owner channel send failed: {e}"))?;
let (host_hwnd, hthumb) = reply_rx.recv()??;
Ok(Self {
host_hwnd,
hthumb,
disposed: false,
})
}
pub fn host_hwnd(&self) -> isize {
self.host_hwnd
}
pub fn update_rect(&self, rect: Rect) -> eyre::Result<()> {
ghost_owner()
.cmd_tx
.send(GhostCmd::UpdateRect {
host_hwnd: self.host_hwnd,
hthumb: self.hthumb,
rect,
})
.map_err(|e| eyre::eyre!("ghost owner channel send failed: {e}"))
}
/// Apply an opacity change directly via `DwmUpdateThumbnailProperties` on
/// the calling thread. Unlike rect updates (which call `SetWindowPos` and
/// therefore need the owner thread), opacity-only updates don't have
/// thread affinity, and going through the channel introduces a race where
/// the next `DwmFlush()` on the caller's thread can fire before the owner
/// has processed the SetOpacity command — which collapses what should be
/// a multi-frame fade into a single visible step.
pub fn set_opacity(&self, opacity: u8) -> eyre::Result<()> {
let props = DWM_THUMBNAIL_PROPERTIES {
dwFlags: DWM_TNP_OPACITY | DWM_TNP_VISIBLE,
rcDestination: RECT::default(),
rcSource: RECT::default(),
opacity,
fVisible: true.into(),
fSourceClientAreaOnly: false.into(),
};
WindowsApi::dwm_update_thumbnail_properties(self.hthumb, &props)
}
pub fn dispose(mut self) -> eyre::Result<()> {
self.dispose_inner()
}
fn dispose_inner(&mut self) -> eyre::Result<()> {
if self.disposed {
return Ok(());
}
self.disposed = true;
ghost_owner()
.cmd_tx
.send(GhostCmd::Destroy {
host_hwnd: self.host_hwnd,
hthumb: self.hthumb,
})
.map_err(|e| eyre::eyre!("ghost owner channel send failed: {e}"))
}
}
impl Drop for GhostWindow {
fn drop(&mut self) {
let _ = self.dispose_inner();
}
}

View File

@@ -13,7 +13,6 @@ use parking_lot::Mutex;
pub use engine::AnimationEngine;
pub mod animation_manager;
pub mod engine;
pub mod ghost;
pub mod lerp;
pub mod prefix;
pub mod render_dispatcher;
@@ -26,33 +25,8 @@ use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Animation configuration
///
/// This can be either global:
/// ```json
/// {
/// "enabled": true,
/// "style": "EaseInSine",
/// "fps": 60,
/// "duration": 250
/// }
/// ```
///
/// Or scoped by an animation kind prefix:
/// ```json
/// {
/// "movement": {
/// "enabled": true,
/// "style": "EaseInSine",
/// "fps": 60,
/// "duration": 250
/// }
/// }
/// ```
pub enum PerAnimationPrefixConfig<T> {
/// Animation configuration prefixed for a specific animation kind
Prefix(HashMap<AnimationPrefix, T>),
/// Animation configuration for all animation kinds
Global(T),
}
@@ -60,7 +34,6 @@ pub const DEFAULT_ANIMATION_ENABLED: bool = false;
pub const DEFAULT_ANIMATION_STYLE: AnimationStyle = AnimationStyle::Linear;
pub const DEFAULT_ANIMATION_DURATION: u64 = 250;
pub const DEFAULT_ANIMATION_FPS: u64 = 60;
pub const DEFAULT_GHOST_MOVEMENT: bool = true;
lazy_static! {
pub static ref ANIMATION_MANAGER: Arc<Mutex<AnimationManager>> =
@@ -80,4 +53,3 @@ lazy_static! {
}
pub static ANIMATION_FPS: AtomicU64 = AtomicU64::new(DEFAULT_ANIMATION_FPS);
pub static GHOST_MOVEMENT_ENABLED: AtomicBool = AtomicBool::new(DEFAULT_GHOST_MOVEMENT);

View File

@@ -5,10 +5,4 @@ pub trait RenderDispatcher {
fn pre_render(&self) -> eyre::Result<()>;
fn render(&self, delta: f64) -> eyre::Result<()>;
fn post_render(&self) -> eyre::Result<()>;
/// Called by the animation engine when an in-flight animation is cancelled
/// before it could complete. Implementors should use this to release any
/// resources allocated in `pre_render` and bring the underlying window
/// back to a consistent visible state. Default: no-op.
fn cleanup_on_cancel(&self) {}
}

View File

@@ -420,7 +420,6 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t),
AnimationStyle::EaseOutCubic => EaseOutCubic::evaluate(t),
AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t),
AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t),

View File

@@ -11,9 +11,7 @@ use crate::core::Rect;
use crate::windows_api;
use std::collections::HashMap;
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::mpsc;
use windows::Win32::Foundation::FALSE;
@@ -58,7 +56,6 @@ use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PostMessageW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
@@ -68,21 +65,11 @@ use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
use windows::Win32::UI::WindowsAndMessaging::WM_USER;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows_core::BOOL;
use windows_core::PCWSTR;
use windows_numerics::Matrix3x2;
/// Custom WM_USER message that tells the border window thread to call update_brushes() on itself,
/// avoiding a data race between the border manager thread and the border's message loop thread.
pub const WM_UPDATE_BRUSHES: u32 = WM_USER + 1;
/// Custom WM_USER message used to drive the border in lockstep with an active
/// movement animation. lparam carries a `Box<Rect>` ownership transfer that the
/// receiving WndProc reclaims and applies as the new tracked rect.
pub const WM_ANIMATE_RECT: u32 = WM_USER + 2;
pub struct RenderFactory(ID2D1Factory);
unsafe impl Sync for RenderFactory {}
unsafe impl Send for RenderFactory {}
@@ -111,98 +98,6 @@ static BRUSH_PROPERTIES: LazyLock<D2D1_BRUSH_PROPERTIES> =
transform: Matrix3x2::identity(),
});
/// Apply a new tracked rect to the border on its own message-loop thread.
/// Updates `window_rect`, calls `set_position`, and re-renders if size/position
/// changed. Used by both `EVENT_OBJECT_LOCATIONCHANGE` (real window movements)
/// and `WM_ANIMATE_RECT` (animation-driven movements while the source is cloaked).
///
/// SAFETY: caller must ensure `border_pointer` is non-null, points to a live
/// `Border`, and that we are running on the border's WndProc thread.
unsafe fn apply_tracked_rect(border_pointer: *mut Border, rect: Rect) {
unsafe {
let reference_hwnd = (*border_pointer).tracking_hwnd;
let old_rect = (*border_pointer).window_rect;
(*border_pointer).window_rect = rect;
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
tracing::error!("failed to update border position {error}");
}
if (rect.is_same_size_as(&old_rect) && rect.has_same_position_as(&old_rect))
|| (*border_pointer).render_target.is_none()
{
return;
}
// double-check destruction flag before rendering
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
return;
}
let render_target = match (*border_pointer).render_target.as_ref() {
Some(rt) => rt,
None => return,
};
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
left: (border_width / 2 - border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
right: (rect.right - border_width / 2 + border_offset) as f32,
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
};
let _ = render_target.Resize(&D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
});
let window_kind = (*border_pointer).window_kind;
let Some(brush) = (*border_pointer).brushes.get(&window_kind) else {
return;
};
render_target.BeginDraw();
render_target.Clear(None);
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);
}
}
pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
let hwnd = hwnd.0 as isize;
@@ -231,7 +126,6 @@ pub struct Border {
pub brush_properties: D2D1_BRUSH_PROPERTIES,
pub rounded_rect: D2D1_ROUNDED_RECT,
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
pub is_destroying: Arc<AtomicBool>,
}
impl From<isize> for Border {
@@ -250,7 +144,6 @@ impl From<isize> for Border {
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
rounded_rect: D2D1_ROUNDED_RECT::default(),
brushes: HashMap::new(),
is_destroying: Arc::new(AtomicBool::new(false)),
}
}
}
@@ -299,7 +192,6 @@ impl Border {
brush_properties: Default::default(),
rounded_rect: Default::default(),
brushes: HashMap::new(),
is_destroying: Arc::new(AtomicBool::new(false)),
};
let border_pointer = &raw mut border;
@@ -421,54 +313,9 @@ impl Border {
}
pub fn destroy(&self) -> color_eyre::Result<()> {
// signal that we're destroying - prevents new render operations from starting
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));
// WM_DESTROY will clear GWLP_USERDATA and drop the render target before D2D
// frees its internal HwndPresenter during WM_NCDESTROY
WindowsApi::close_window(self.hwnd)
}
/// Post a message to the border's own message loop thread requesting a brush update.
/// This ensures update_brushes() always runs on the window thread that owns the D2D
/// render target, preventing a data race with concurrent WndProc render operations.
pub fn request_brush_update(&self) {
let _ = unsafe {
PostMessageW(
Option::from(self.hwnd()),
WM_UPDATE_BRUSHES,
WPARAM(0),
LPARAM(0),
)
};
}
/// Drive the border to follow `rect` during a movement animation. Hands
/// ownership of a boxed `Rect` to the border's message-loop thread via
/// `WM_ANIMATE_RECT`, which mirrors the redraw path normally driven by
/// `EVENT_OBJECT_LOCATIONCHANGE` on the real source window.
pub fn animate_to(&self, rect: Rect) {
let boxed = Box::new(rect);
let ptr = Box::into_raw(boxed);
let posted = unsafe {
PostMessageW(
Option::from(self.hwnd()),
WM_ANIMATE_RECT,
WPARAM(0),
LPARAM(ptr as isize),
)
};
if posted.is_err() {
// Reclaim the box on failure to avoid leaking.
unsafe {
drop(Box::from_raw(ptr));
}
}
}
pub fn set_position(&self, rect: &Rect, reference_hwnd: isize) -> color_eyre::Result<()> {
let mut rect = *rect;
rect.add_margin(self.width);
@@ -534,29 +381,77 @@ impl Border {
return LRESULT(0);
}
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
return LRESULT(0);
}
let reference_hwnd = (*border_pointer).tracking_hwnd;
let old_rect = (*border_pointer).window_rect;
let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default();
apply_tracked_rect(border_pointer, rect);
LRESULT(0)
}
WM_ANIMATE_RECT => {
// lparam carries an owned Box<Rect> from the animation thread.
let rect_box = Box::from_raw(lparam.0 as *mut Rect);
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
(*border_pointer).window_rect = rect;
if let Err(error) = (*border_pointer).set_position(&rect, reference_hwnd) {
tracing::error!("failed to update border position {error}");
}
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
return LRESULT(0);
if (!rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect))
&& let Some(render_target) = (*border_pointer).render_target.as_ref()
{
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;
(*border_pointer).rounded_rect.rect = D2D_RECT_F {
left: (border_width / 2 - border_offset) as f32,
top: (border_width / 2 - border_offset) as f32,
right: (rect.right - border_width / 2 + border_offset) as f32,
bottom: (rect.bottom - border_width / 2 + border_offset) as f32,
};
let _ = render_target.Resize(&D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
});
let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
render_target.BeginDraw();
render_target.Clear(None);
// Calculate border radius based on style
let style = match (*border_pointer).style {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
} else {
BorderStyle::Square
}
}
BorderStyle::Rounded => BorderStyle::Rounded,
BorderStyle::Square => BorderStyle::Square,
};
match style {
BorderStyle::Rounded => {
render_target.DrawRoundedRectangle(
&(*border_pointer).rounded_rect,
brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
render_target.DrawRectangle(
&(*border_pointer).rounded_rect.rect,
brush,
border_width as f32,
None,
);
}
_ => {}
}
let _ = render_target.EndDraw(None, None);
}
}
apply_tracked_rect(border_pointer, *rect_box);
LRESULT(0)
}
WM_PAINT => {
@@ -568,10 +463,6 @@ impl Border {
return LRESULT(0);
}
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
return LRESULT(0);
}
let reference_hwnd = (*border_pointer).tracking_hwnd;
// Update position to update the ZOrder
@@ -585,11 +476,6 @@ impl Border {
}
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
// 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).offset = BORDER_OFFSET.load(Ordering::Relaxed);
@@ -656,27 +542,8 @@ impl Border {
let _ = ValidateRect(Option::from(window), None);
LRESULT(0)
}
WM_UPDATE_BRUSHES => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if border_pointer.is_null() {
return LRESULT(0);
}
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
return LRESULT(0);
}
if let Err(error) = (*border_pointer).update_brushes() {
tracing::error!("failed to update brushes: {error}");
}
(*border_pointer).invalidate();
LRESULT(0)
}
WM_DESTROY => {
let border_pointer: *mut Border = GetWindowLongPtrW(window, GWLP_USERDATA) as _;
if !border_pointer.is_null() {
(*border_pointer).render_target = None;
(*border_pointer).brushes.clear();
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
}
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
PostQuitMessage(0);
LRESULT(0)
}

View File

@@ -113,20 +113,6 @@ pub fn window_border(hwnd: isize) -> Option<BorderInfo> {
})
}
/// Drive the border that tracks `source_hwnd` to follow `rect`. No-op when no
/// border is registered for the source window. Used by movement animations to
/// keep the border visually in sync while the source window is cloaked.
pub fn animate_to(source_hwnd: isize, rect: crate::core::Rect) {
let border_id = match WINDOWS_BORDERS.lock().get(&source_hwnd).cloned() {
Some(id) => id,
None => return,
};
let state = BORDER_STATE.lock();
if let Some(border) = state.get(&border_id) {
border.animate_to(rect);
}
}
pub fn send_notification(hwnd: Option<isize>) {
if event_tx().try_send(Notification::Update(hwnd)).is_err() {
tracing::warn!("channel is full; dropping notification")
@@ -465,11 +451,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
} 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).
// Post to the border's own thread to avoid a data race between
// this thread dropping the old render target and the window
// thread mid-render holding a reference to it.
border.request_brush_update();
// already have their brushes updated on creation)
border.update_brushes()?;
}
border.invalidate();
@@ -633,11 +616,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
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).
// Post to the border's own thread to avoid a data race between
// this thread dropping the old render target and the window
// thread mid-render holding a reference to it.
border.request_brush_update();
// already have their brushes updated on creation)
border.update_brushes()?;
}
border.set_position(&rect, focused_window_hwnd)?;
border.invalidate();
@@ -719,11 +699,8 @@ fn handle_floating_borders(
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).
// Post to the border's own thread to avoid a data race between
// this thread dropping the old render target and the window
// thread mid-render holding a reference to it.
border.request_brush_update();
// already have their brushes updated on creation)
border.update_brushes()?;
}
border.set_position(&rect, window.hwnd)?;
border.invalidate();
@@ -790,7 +767,6 @@ fn remove_border(
fn destroy_border(border: Box<Border>) -> color_eyre::Result<()> {
let raw_pointer = Box::into_raw(border);
unsafe {
// Now safe to destroy window
(*raw_pointer).destroy()?;
}
Ok(())
@@ -837,26 +813,10 @@ pub fn hide_border(tracking_hwnd: isize) {
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Z Order (https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos)
pub enum ZOrder {
/// HWND_TOP
///
/// Places the window at the top of the Z order.
Top,
/// HWND_NOTOPMOST
///
/// Places the window above all non-topmost windows (that is, behind all topmost windows).
/// This flag has no effect if the window is already a non-topmost window.
NoTopMost,
/// HWND_BOTTOM
///
/// Places the window at the bottom of the Z order. If the hWnd parameter identifies a topmost window,
/// the window loses its topmost status and is placed at the bottom of all other windows.
Bottom,
/// HWND_TOPMOST
///
/// Places the window above all non-topmost windows.
/// The window maintains its topmost position even when it is deactivated.
TopMost,
}

View File

@@ -41,18 +41,6 @@ impl Lockable for Container {
}
impl Container {
pub fn preselect() -> Self {
Self {
id: "PRESELECT".to_string(),
locked: false,
windows: Default::default(),
}
}
pub fn is_preselect(&self) -> bool {
self.id == "PRESELECT"
}
pub fn hide(&self, omit: Option<isize>) {
for window in self.windows().iter().rev() {
let mut should_hide = omit.is_none();

View File

@@ -8,73 +8,38 @@ use strum::EnumString;
#[derive(Copy, Clone, Debug, Display, EnumString, ValueEnum, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Mathematical function which describes the rate at which a value changes
pub enum AnimationStyle {
/// Linear
Linear,
/// Ease in sine
EaseInSine,
/// Ease out sine
EaseOutSine,
/// Ease in out sine
EaseInOutSine,
/// Ease in quad
EaseInQuad,
/// Ease out quad
EaseOutQuad,
/// Ease in out quad
EaseInOutQuad,
/// Ease in cubic
EaseInCubic,
/// Ease out cubic
EaseOutCubic,
/// Ease in out cubic
EaseInOutCubic,
/// Ease in quart
EaseInQuart,
/// Ease out quart
EaseOutQuart,
/// Ease in out quart
EaseInOutQuart,
/// Ease in quint
EaseInQuint,
/// Ease out quint
EaseOutQuint,
/// Ease in out quint
EaseInOutQuint,
/// Ease in expo
EaseInExpo,
/// Ease out expo
EaseOutExpo,
/// Ease in out expo
EaseInOutExpo,
/// Ease in circ
EaseInCirc,
/// Ease out circ
EaseOutCirc,
/// Ease in out circ
EaseInOutCirc,
/// Ease in back
EaseInBack,
/// Ease out back
EaseOutBack,
/// Ease in out back
EaseInOutBack,
/// Ease in elastic
EaseInElastic,
/// Ease out elastic
EaseOutElastic,
/// Ease in out elastic
EaseInOutElastic,
/// Ease in bounce
EaseInBounce,
/// Ease out bounce
EaseOutBounce,
/// Ease in out bounce
EaseInOutBounce,
#[cfg_attr(feature = "schemars", schemars(title = "CubicBezier"))]
#[value(skip)]
/// Custom Cubic Bezier function
CubicBezier(f64, f64, f64, f64),
}

View File

@@ -6,22 +6,13 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[cfg(feature = "win32")]
use super::CustomLayout;
use super::DefaultLayout;
use super::Rect;
#[cfg(feature = "win32")]
use super::custom_layout::Column;
#[cfg(feature = "win32")]
use super::custom_layout::ColumnSplit;
#[cfg(feature = "win32")]
use super::custom_layout::ColumnSplitWithCapacity;
use crate::default_layout::DEFAULT_RATIO;
use crate::default_layout::DEFAULT_SECONDARY_RATIO;
use crate::default_layout::LayoutOptions;
use crate::default_layout::MAX_RATIO;
use crate::default_layout::MAX_RATIOS;
use crate::default_layout::MIN_RATIO;
pub trait Arrangement {
#[allow(clippy::too_many_arguments)]
@@ -51,142 +42,132 @@ impl Arrangement for DefaultLayout {
layout_options: Option<LayoutOptions>,
latest_layout: &[Rect],
) -> Vec<Rect> {
// Trace layout_options for debugging
if let Some(ref opts) = layout_options {
tracing::debug!(
"Layout {:?} - layout_options received: column_ratios={:?}, row_ratios={:?}",
self,
opts.column_ratios,
opts.row_ratios
);
} else {
tracing::debug!("Layout {:?} - no layout_options provided", self);
}
let len = usize::from(len);
let mut dimensions = match self {
Self::Scrolling => {
let column_count = layout_options
.as_ref()
.and_then(|o| o.scrolling.map(|s| s.columns))
.unwrap_or(3);
let column_width = area.right / column_count.min(len) as i32;
let column_width = area.right / column_count as i32;
let mut layouts = Vec::with_capacity(len);
let visible_columns = area.right / column_width;
let keep_centered = layout_options
.as_ref()
.and_then(|o| {
o.scrolling
.map(|s| s.center_focused_column.unwrap_or_default())
})
.unwrap_or(false);
match len {
// treat < 3 windows the same as the columns layout
len if len < 3 => {
layouts = columns(area, len);
let first_visible: isize = if focused_idx == 0 {
// if focused idx is 0, we are at the beginning of the scrolling strip
0
} else {
let previous_first_visible = if latest_layout.is_empty() {
0
} else {
// previous first_visible based on the left position of the first visible window
let left_edge = area.left;
latest_layout
.iter()
.position(|rect| rect.left >= left_edge)
.unwrap_or(0) as isize
};
let adjustment = calculate_columns_adjustment(resize_dimensions);
layouts.iter_mut().zip(adjustment.iter()).for_each(
|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
},
);
let focused_idx = focused_idx as isize;
// if center_focused_column is enabled, and we have an odd number of visible columns,
// center the focused window column
if keep_centered && visible_columns % 2 == 1 {
let center_offset = visible_columns as isize / 2;
(focused_idx - center_offset).max(0).min(
(len as isize)
.saturating_sub(visible_columns as isize)
.max(0),
)
} else {
if focused_idx < previous_first_visible {
// focused window is off the left edge, we need to scroll left
focused_idx
} else if focused_idx >= previous_first_visible + visible_columns as isize {
// focused window is off the right edge, we need to scroll right
// and make sure it's the last visible window
(focused_idx + 1 - visible_columns as isize).max(0)
} else {
// focused window is already visible, we don't need to scroll
previous_first_visible
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) && let 2.. = len
{
columns_reverse(&mut layouts);
}
.min(
(len as isize)
.saturating_sub(visible_columns as isize)
.max(0),
)
}
};
// treat >= column_count as scrolling
len => {
let visible_columns = area.right / column_width;
let keep_centered = layout_options
.and_then(|o| {
o.scrolling
.map(|s| s.center_focused_column.unwrap_or_default())
})
.unwrap_or(false);
for i in 0..len {
let position = (i as isize) - first_visible;
let left = area.left + (position as i32 * column_width);
let first_visible: isize = if focused_idx == 0 {
// if focused idx is 0, we are at the beginning of the scrolling strip
0
} else {
let previous_first_visible = if latest_layout.is_empty() {
0
} else {
// previous first_visible based on the left position of the first visible window
let left_edge = area.left;
latest_layout
.iter()
.position(|rect| rect.left >= left_edge)
.unwrap_or(0) as isize
};
layouts.push(Rect {
left,
top: area.top,
right: column_width,
bottom: area.bottom,
});
let focused_idx = focused_idx as isize;
// if center_focused_column is enabled, and we have an odd number of visible columns,
// center the focused window column
if keep_centered && visible_columns % 2 == 1 {
let center_offset = visible_columns as isize / 2;
(focused_idx - center_offset).max(0).min(
(len as isize)
.saturating_sub(visible_columns as isize)
.max(0),
)
} else {
if focused_idx < previous_first_visible {
// focused window is off the left edge, we need to scroll left
focused_idx
} else if focused_idx
>= previous_first_visible + visible_columns as isize
{
// focused window is off the right edge, we need to scroll right
// and make sure it's the last visible window
(focused_idx + 1 - visible_columns as isize).max(0)
} else {
// focused window is already visible, we don't need to scroll
previous_first_visible
}
.min(
(len as isize)
.saturating_sub(visible_columns as isize)
.max(0),
)
}
};
for i in 0..len {
let position = (i as isize) - first_visible;
let left = area.left + (position as i32 * column_width);
layouts.push(Rect {
left,
top: area.top,
right: column_width,
bottom: area.bottom,
});
}
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
layouts.iter_mut().zip(adjustment.iter()).for_each(
|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
},
);
}
}
// Last visible column absorbs any remainder from integer division
// so that visible columns tile the full area width without gaps
let width_remainder = area.right - column_width * visible_columns;
if width_remainder > 0 {
let last_visible_idx =
(first_visible as usize + visible_columns as usize - 1).min(len - 1);
layouts[last_visible_idx].right += width_remainder;
}
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
layouts
}
Self::BSP => {
let column_split_ratio = layout_options
.and_then(|o| o.column_ratios)
.and_then(|r| r[0])
.unwrap_or(DEFAULT_RATIO)
.clamp(MIN_RATIO, MAX_RATIO);
let row_split_ratio = layout_options
.and_then(|o| o.row_ratios)
.and_then(|r| r[0])
.unwrap_or(DEFAULT_RATIO)
.clamp(MIN_RATIO, MAX_RATIO);
recursive_fibonacci(
0,
len,
area,
layout_flip,
calculate_resize_adjustments(resize_dimensions),
column_split_ratio,
row_split_ratio,
)
}
Self::BSP => recursive_fibonacci(
0,
len,
area,
layout_flip,
calculate_resize_adjustments(resize_dimensions),
),
Self::Columns => {
let ratios = layout_options.and_then(|o| o.column_ratios);
let mut layouts = columns_with_ratios(area, len, ratios);
let mut layouts = columns(area, len);
let adjustment = calculate_columns_adjustment(resize_dimensions);
layouts
@@ -210,8 +191,7 @@ impl Arrangement for DefaultLayout {
layouts
}
Self::Rows => {
let ratios = layout_options.and_then(|o| o.row_ratios);
let mut layouts = rows_with_ratios(area, len, ratios);
let mut layouts = rows(area, len);
let adjustment = calculate_rows_adjustment(resize_dimensions);
layouts
@@ -237,17 +217,9 @@ impl Arrangement for DefaultLayout {
Self::VerticalStack => {
let mut layouts: Vec<Rect> = vec![];
#[allow(clippy::cast_possible_truncation)]
let primary_right = match len {
1 => area.right,
_ => {
let ratio = layout_options
.and_then(|o| o.column_ratios)
.and_then(|r| r[0])
.unwrap_or(DEFAULT_RATIO)
.clamp(MIN_RATIO, MAX_RATIO);
(area.right as f32 * ratio) as i32
}
_ => area.right / 2,
};
let main_left = area.left;
@@ -262,8 +234,7 @@ impl Arrangement for DefaultLayout {
});
if len > 1 {
let row_ratios = layout_options.and_then(|o| o.row_ratios);
layouts.append(&mut rows_with_ratios(
layouts.append(&mut rows(
&Rect {
left: stack_left,
top: area.top,
@@ -271,7 +242,6 @@ impl Arrangement for DefaultLayout {
bottom: area.bottom,
},
len - 1,
row_ratios,
));
}
}
@@ -315,17 +285,9 @@ impl Arrangement for DefaultLayout {
// Shamelessly borrowed from LeftWM: https://github.com/leftwm/leftwm/commit/f673851745295ae7584a102535566f559d96a941
let mut layouts: Vec<Rect> = vec![];
#[allow(clippy::cast_possible_truncation)]
let primary_width = match len {
1 => area.right,
_ => {
let ratio = layout_options
.and_then(|o| o.column_ratios)
.and_then(|r| r[0])
.unwrap_or(DEFAULT_RATIO)
.clamp(MIN_RATIO, MAX_RATIO);
(area.right as f32 * ratio) as i32
}
_ => area.right / 2,
};
let primary_left = match len {
@@ -342,8 +304,7 @@ impl Arrangement for DefaultLayout {
});
if len > 1 {
let row_ratios = layout_options.and_then(|o| o.row_ratios);
layouts.append(&mut rows_with_ratios(
layouts.append(&mut rows(
&Rect {
left: area.left,
top: area.top,
@@ -351,7 +312,6 @@ impl Arrangement for DefaultLayout {
bottom: area.bottom,
},
len - 1,
row_ratios,
));
}
}
@@ -394,17 +354,9 @@ impl Arrangement for DefaultLayout {
Self::HorizontalStack => {
let mut layouts: Vec<Rect> = vec![];
#[allow(clippy::cast_possible_truncation)]
let bottom = match len {
1 => area.bottom,
_ => {
let ratio = layout_options
.and_then(|o| o.row_ratios)
.and_then(|r| r[0])
.unwrap_or(DEFAULT_RATIO)
.clamp(MIN_RATIO, MAX_RATIO);
(area.bottom as f32 * ratio) as i32
}
_ => area.bottom / 2,
};
let main_top = area.top;
@@ -419,8 +371,7 @@ impl Arrangement for DefaultLayout {
});
if len > 1 {
let col_ratios = layout_options.and_then(|o| o.column_ratios);
layouts.append(&mut columns_with_ratios(
layouts.append(&mut columns(
&Rect {
left: area.left,
top: stack_top,
@@ -428,7 +379,6 @@ impl Arrangement for DefaultLayout {
bottom: area.bottom - bottom,
},
len - 1,
col_ratios,
));
}
}
@@ -471,28 +421,15 @@ impl Arrangement for DefaultLayout {
Self::UltrawideVerticalStack => {
let mut layouts: Vec<Rect> = vec![];
// Get ratios: [0] = primary (center), [1] = secondary (left), remainder = tertiary (right)
let ratios = layout_options.and_then(|o| o.column_ratios);
let primary_ratio = ratios
.and_then(|r| r[0])
.unwrap_or(DEFAULT_RATIO)
.clamp(MIN_RATIO, MAX_RATIO);
let secondary_ratio = ratios
.and_then(|r| r[1])
.unwrap_or(DEFAULT_SECONDARY_RATIO)
.clamp(MIN_RATIO, MAX_RATIO);
#[allow(clippy::cast_possible_truncation)]
let primary_right = match len {
1 => area.right,
_ => (area.right as f32 * primary_ratio) as i32,
_ => area.right / 2,
};
#[allow(clippy::cast_possible_truncation)]
let secondary_right = match len {
1 => 0,
2 => area.right - primary_right,
_ => (area.right as f32 * secondary_ratio) as i32,
_ => (area.right - primary_right) / 2,
};
let (primary_left, secondary_left, stack_left) = match len {
@@ -529,18 +466,14 @@ impl Arrangement for DefaultLayout {
});
if len > 2 {
// Tertiary column gets remaining space after primary and secondary
let tertiary_right = area.right - primary_right - secondary_right;
let row_ratios = layout_options.and_then(|o| o.row_ratios);
layouts.append(&mut rows_with_ratios(
layouts.append(&mut rows(
&Rect {
left: stack_left,
top: area.top,
right: tertiary_right,
right: secondary_right,
bottom: area.bottom,
},
len - 2,
row_ratios,
));
}
}
@@ -609,94 +542,13 @@ impl Arrangement for DefaultLayout {
let len = len as i32;
let row_constraint = layout_options.as_ref().and_then(|o| o.grid.map(|g| g.rows));
let column_ratios = layout_options.and_then(|o| o.column_ratios);
// Count defined column ratios (already validated at deserialization to sum < 1.0)
let defined_ratios = column_ratios
.as_ref()
.map(|r| r.iter().filter(|x| x.is_some()).count())
.unwrap_or(0);
let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
let num_cols = if let Some(rows) = row_constraint {
((len as f32) / (rows as f32)).ceil() as i32
} else {
(len as f32).sqrt().ceil() as i32
};
// Pre-calculate column widths and left positions using same logic as columns_with_ratios
let mut col_widths: Vec<i32> = Vec::with_capacity(num_cols as usize);
let mut col_lefts: Vec<i32> = Vec::with_capacity(num_cols as usize);
let mut current_left = area.left;
for col in 0..num_cols {
let col_idx = col as usize;
let width = if let Some(ref ratios) = column_ratios {
// Only apply ratio if there's at least one more column after this
// The last column always gets the remaining space
let should_apply_ratio =
col_idx < MAX_RATIOS && col_idx < defined_ratios && col < num_cols - 1;
if should_apply_ratio {
if let Some(ratio) = ratios[col_idx] {
(area.right as f32 * ratio) as i32
} else {
let used: f32 = (0..col_idx).filter_map(|j| ratios[j]).sum();
let remaining_space =
area.right - (area.right as f32 * used) as i32;
let remaining_cols = num_cols - col;
remaining_space / remaining_cols
}
} else {
// Beyond defined ratios or last column - split remaining space equally
// Only count ratios that were actually applied (up to defined_ratios, but not beyond num_cols - 1)
let ratios_applied = defined_ratios.min((num_cols - 1) as usize);
let used: f32 = (0..ratios_applied).filter_map(|j| ratios[j]).sum();
let remaining_space = area.right - (area.right as f32 * used) as i32;
let remaining_cols = (num_cols as usize - ratios_applied) as i32;
if remaining_cols > 0 {
remaining_space / remaining_cols
} else {
remaining_space
}
}
} else {
area.right / num_cols
};
col_lefts.push(current_left);
col_widths.push(width);
current_left += width;
}
// Last column absorbs any remainder from integer division
// so that columns tile the full area width without gaps
let total_width: i32 = col_widths.iter().sum();
let width_remainder = area.right - total_width;
if width_remainder > 0
&& let Some(last) = col_widths.last_mut()
{
*last += width_remainder;
}
// Pre-calculate flipped column positions: same widths laid out
// in reverse order so that the last column sits at area.left
let flipped_col_lefts = if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
let n = num_cols as usize;
let mut flipped = vec![0i32; n];
let mut fl = area.left;
for i in (0..n).rev() {
flipped[i] = fl;
fl += col_widths[i];
}
flipped
} else {
vec![]
};
let mut iter = layouts.iter_mut().enumerate().peekable();
for col in 0..num_cols {
@@ -710,47 +562,26 @@ impl Arrangement for DefaultLayout {
remaining_windows / remaining_columns
};
// Rows within each column: base height from integer division,
// last row absorbs any remainder to cover the full area height
let base_height = area.bottom / num_rows_in_this_col;
let height_remainder = area.bottom - base_height * num_rows_in_this_col;
let col_idx = col as usize;
let win_width = col_widths[col_idx];
let col_left = col_lefts[col_idx];
let win_height = area.bottom / num_rows_in_this_col;
let win_width = area.right / num_cols;
for row in 0..num_rows_in_this_col {
if let Some((_idx, win)) = iter.next() {
let is_last_row = row == num_rows_in_this_col - 1;
let win_height = if is_last_row {
base_height + height_remainder
} else {
base_height
};
let mut left = col_left;
let mut top = area.top + base_height * row;
let mut left = area.left + win_width * col;
let mut top = area.top + win_height * row;
match layout_flip {
Some(Axis::Horizontal) => {
left = flipped_col_lefts[col_idx];
left = area.right - win_width * (col + 1) + area.left;
}
Some(Axis::Vertical) => {
top = if is_last_row {
area.top
} else {
area.top + area.bottom - base_height * (row + 1)
};
top = area.bottom - win_height * (row + 1) + area.top;
}
Some(Axis::HorizontalAndVertical) => {
left = flipped_col_lefts[col_idx];
top = if is_last_row {
area.top
} else {
area.top + area.bottom - base_height * (row + 1)
};
left = area.right - win_width * (col + 1) + area.left;
top = area.bottom - win_height * (row + 1) + area.top;
}
None => {}
None => {} // No flip
}
win.bottom = win_height;
@@ -773,7 +604,6 @@ impl Arrangement for DefaultLayout {
}
}
#[cfg(feature = "win32")]
impl Arrangement for CustomLayout {
fn calculate(
&self,
@@ -902,78 +732,20 @@ impl Arrangement for CustomLayout {
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Axis on which to perform an operation
pub enum Axis {
/// Horizontal axis
Horizontal,
/// Vertical axis
Vertical,
/// Both horizontal and vertical axes
HorizontalAndVertical,
}
#[cfg(feature = "win32")]
#[must_use]
fn columns(area: &Rect, len: usize) -> Vec<Rect> {
columns_with_ratios(area, len, None)
}
#[must_use]
fn columns_with_ratios(
area: &Rect,
len: usize,
ratios: Option<[Option<f32>; MAX_RATIOS]>,
) -> Vec<Rect> {
tracing::debug!(
"columns_with_ratios called: len={}, ratios={:?}",
len,
ratios
);
let mut layouts: Vec<Rect> = vec![];
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let right = area.right / len as i32;
let mut left = 0;
// Count how many ratios are defined (already validated at deserialization to sum < 1.0)
let defined_ratios = ratios
.as_ref()
.map(|r| r.iter().filter(|x| x.is_some()).count())
.unwrap_or(0);
for i in 0..len {
#[allow(clippy::cast_possible_truncation)]
let right = if let Some(ref r) = ratios {
// Only apply ratio[i] if there's at least one more column after this (i < len - 1)
// The last column always gets the remaining space
let should_apply_ratio = i < MAX_RATIOS && i < defined_ratios && i < len - 1;
if should_apply_ratio {
if let Some(ratio) = r[i] {
(area.right as f32 * ratio) as i32
} else {
let used: f32 = (0..i).filter_map(|j| r[j]).sum();
let remaining_space = area.right - (area.right as f32 * used) as i32;
let remaining_columns = len - i;
remaining_space / remaining_columns as i32
}
} else {
// Last column or beyond defined ratios - split remaining space equally
let ratios_applied = i.min(defined_ratios).min(len.saturating_sub(1));
let used: f32 = (0..ratios_applied).filter_map(|j| r[j]).sum();
let remaining_space = area.right - (area.right as f32 * used) as i32;
let remaining_columns = len - ratios_applied;
if remaining_columns > 0 {
remaining_space / remaining_columns as i32
} else {
remaining_space
}
}
} else {
// Equal width columns (original behavior)
#[allow(clippy::cast_possible_wrap)]
{
area.right / len as i32
}
};
let mut layouts: Vec<Rect> = vec![];
for _ in 0..len {
layouts.push(Rect {
left: area.left + left,
top: area.top,
@@ -984,77 +756,17 @@ fn columns_with_ratios(
left += right;
}
// Last column absorbs any remainder from integer division
// so that columns tile the full area width without gaps
let total_width: i32 = layouts.iter().map(|r| r.right).sum();
let remainder = area.right - total_width;
if remainder > 0
&& let Some(last) = layouts.last_mut()
{
last.right += remainder;
}
layouts
}
#[cfg(feature = "win32")]
#[must_use]
fn rows(area: &Rect, len: usize) -> Vec<Rect> {
rows_with_ratios(area, len, None)
}
#[must_use]
fn rows_with_ratios(
area: &Rect,
len: usize,
ratios: Option<[Option<f32>; MAX_RATIOS]>,
) -> Vec<Rect> {
tracing::debug!("rows_with_ratios called: len={}, ratios={:?}", len, ratios);
let mut layouts: Vec<Rect> = vec![];
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let bottom = area.bottom / len as i32;
let mut top = 0;
// Count how many ratios are defined (already validated at deserialization to sum < 1.0)
let defined_ratios = ratios
.as_ref()
.map(|r| r.iter().filter(|x| x.is_some()).count())
.unwrap_or(0);
for i in 0..len {
#[allow(clippy::cast_possible_truncation)]
let bottom = if let Some(ref r) = ratios {
// Only apply ratio[i] if there's at least one more row after this (i < len - 1)
// The last row always gets the remaining space
let should_apply_ratio = i < MAX_RATIOS && i < defined_ratios && i < len - 1;
if should_apply_ratio {
if let Some(ratio) = r[i] {
(area.bottom as f32 * ratio) as i32
} else {
let used: f32 = (0..i).filter_map(|j| r[j]).sum();
let remaining_space = area.bottom - (area.bottom as f32 * used) as i32;
let remaining_rows = len - i;
remaining_space / remaining_rows as i32
}
} else {
// Last row or beyond defined ratios - split remaining space equally
let ratios_applied = i.min(defined_ratios).min(len.saturating_sub(1));
let used: f32 = (0..ratios_applied).filter_map(|j| r[j]).sum();
let remaining_space = area.bottom - (area.bottom as f32 * used) as i32;
let remaining_rows = len - ratios_applied;
if remaining_rows > 0 {
remaining_space / remaining_rows as i32
} else {
remaining_space
}
}
} else {
// Equal height rows (original behavior)
#[allow(clippy::cast_possible_wrap)]
{
area.bottom / len as i32
}
};
let mut layouts: Vec<Rect> = vec![];
for _ in 0..len {
layouts.push(Rect {
left: area.left,
top: area.top + top,
@@ -1065,16 +777,6 @@ fn rows_with_ratios(
top += bottom;
}
// Last row absorbs any remainder from integer division
// so that rows tile the full area height without gaps
let total_height: i32 = layouts.iter().map(|r| r.bottom).sum();
let remainder = area.bottom - total_height;
if remainder > 0
&& let Some(last) = layouts.last_mut()
{
last.bottom += remainder;
}
layouts
}
@@ -1184,8 +886,6 @@ fn recursive_fibonacci(
area: &Rect,
layout_flip: Option<Axis>,
resize_adjustments: Vec<Option<Rect>>,
column_split_ratio: f32,
row_split_ratio: f32,
) -> Vec<Rect> {
let mut a = *area;
@@ -1199,41 +899,41 @@ fn recursive_fibonacci(
*area
};
#[allow(clippy::cast_possible_truncation)]
let primary_resized_width = (resized.right as f32 * column_split_ratio) as i32;
#[allow(clippy::cast_possible_truncation)]
let primary_resized_height = (resized.bottom as f32 * row_split_ratio) as i32;
let half_width = area.right / 2;
let half_height = area.bottom / 2;
let half_resized_width = resized.right / 2;
let half_resized_height = resized.bottom / 2;
let (main_x, alt_x, alt_y, main_y);
if let Some(flip) = layout_flip {
match flip {
Axis::Horizontal => {
main_x = resized.left + (area.right - primary_resized_width);
main_x = resized.left + half_width + (half_width - half_resized_width);
alt_x = resized.left;
alt_y = resized.top + primary_resized_height;
alt_y = resized.top + half_resized_height;
main_y = resized.top;
}
Axis::Vertical => {
main_y = resized.top + (area.bottom - primary_resized_height);
main_y = resized.top + half_height + (half_height - half_resized_height);
alt_y = resized.top;
main_x = resized.left;
alt_x = resized.left + primary_resized_width;
alt_x = resized.left + half_resized_width;
}
Axis::HorizontalAndVertical => {
main_x = resized.left + (area.right - primary_resized_width);
main_x = resized.left + half_width + (half_width - half_resized_width);
alt_x = resized.left;
main_y = resized.top + (area.bottom - primary_resized_height);
main_y = resized.top + half_height + (half_height - half_resized_height);
alt_y = resized.top;
}
}
} else {
main_x = resized.left;
alt_x = resized.left + primary_resized_width;
alt_x = resized.left + half_resized_width;
main_y = resized.top;
alt_y = resized.top + primary_resized_height;
alt_y = resized.top + half_resized_height;
}
#[allow(clippy::if_not_else)]
@@ -1251,7 +951,7 @@ fn recursive_fibonacci(
left: resized.left,
top: main_y,
right: resized.right,
bottom: primary_resized_height,
bottom: half_resized_height,
}];
res.append(&mut recursive_fibonacci(
idx + 1,
@@ -1260,19 +960,17 @@ fn recursive_fibonacci(
left: area.left,
top: alt_y,
right: area.right,
bottom: area.bottom - primary_resized_height,
bottom: area.bottom - half_resized_height,
},
layout_flip,
resize_adjustments,
column_split_ratio,
row_split_ratio,
));
res
} else {
let mut res = vec![Rect {
left: main_x,
top: resized.top,
right: primary_resized_width,
right: half_resized_width,
bottom: resized.bottom,
}];
res.append(&mut recursive_fibonacci(
@@ -1281,13 +979,11 @@ fn recursive_fibonacci(
&Rect {
left: alt_x,
top: area.top,
right: area.right - primary_resized_width,
right: area.right - half_resized_width,
bottom: area.bottom,
},
layout_flip,
resize_adjustments,
column_split_ratio,
row_split_ratio,
));
res
}
@@ -1595,7 +1291,3 @@ fn resize_top(rect: &mut Rect, resize: i32) {
fn resize_bottom(rect: &mut Rect, resize: i32) {
rect.bottom += resize / 2;
}
#[cfg(test)]
#[path = "arrangement_tests.rs"]
mod tests;

View File

@@ -53,64 +53,41 @@ impl ApplicationOptions {
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Rule for matching applications
pub enum MatchingRule {
/// Simple matching rule which must evaluate to true
Simple(IdWithIdentifier),
/// Composite matching rule where all conditions must evaluate to true
Composite(Vec<IdWithIdentifier>),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Rule for assigning applications to a workspace
pub struct WorkspaceMatchingRule {
/// Target monitor index
pub monitor_index: usize,
/// Target workspace index
pub workspace_index: usize,
/// Matching rule for the application
pub matching_rule: MatchingRule,
/// Whether to apply the rule only when the application is initially launched
pub initial_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Rule for matching applications
pub struct IdWithIdentifier {
/// Kind of identifier to target
pub kind: ApplicationIdentifier,
/// Target identifier
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
/// Matching strategy to use
pub matching_strategy: Option<MatchingStrategy>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Strategy for matching identifiers
pub enum MatchingStrategy {
/// Should not be used, only kept for backward compatibility
Legacy,
/// Equals
Equals,
/// Starts With
StartsWith,
/// Ends With
EndsWith,
/// Contains
Contains,
/// Regex
Regex,
/// Does not end with
DoesNotEndWith,
/// Does not start with
DoesNotStartWith,
/// Does not equal
DoesNotEqual,
/// Does not contain
DoesNotContain,
}

View File

@@ -1,5 +1,3 @@
use std::collections::HashMap;
use clap::ValueEnum;
use serde::Deserialize;
use serde::Serialize;
@@ -10,233 +8,34 @@ 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;
/// Validates and converts a Vec of ratios into a fixed-size array.
/// - Clamps values to MIN_RATIO..MAX_RATIO range
/// - Truncates when cumulative sum reaches or exceeds 1.0
/// - Limits to MAX_RATIOS values
#[must_use]
pub fn validate_ratios(ratios: &[f32]) -> [Option<f32>; MAX_RATIOS] {
let mut arr = [None; MAX_RATIOS];
let mut cumulative_sum = 0.0_f32;
for (i, &val) in ratios.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
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Display, EnumString, ValueEnum,
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| validate_ratios(&vec)))
}
/// 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)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Options for specific layouts
pub struct LayoutOptions {
/// Options related to the Scrolling layout
pub scrolling: Option<ScrollingLayoutOptions>,
/// Options related to the Grid layout
pub grid: Option<GridLayoutOptions>,
/// 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,
@@ -246,27 +45,11 @@ pub struct ScrollingLayoutOptions {
#[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,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Per-layout default options entry for the `layout_defaults` global setting.
/// Contains both base layout options and threshold-based layout options rules.
pub struct LayoutDefaultEntry {
/// Default layout options for this layout
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_options: Option<LayoutOptions>,
/// Threshold-based layout options rules in the format of threshold => options.
/// When container count >= threshold, the highest matching threshold's options
/// fully replace the base `layout_options`.
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_options_rules: Option<HashMap<usize, LayoutOptions>>,
}
impl DefaultLayout {
pub fn leftmost_index(&self, len: usize) -> usize {
match self {
@@ -435,7 +218,3 @@ impl DefaultLayout {
}
}
}
#[cfg(test)]
#[path = "default_layout_tests.rs"]
mod tests;

View File

@@ -1,12 +1,8 @@
use super::DefaultLayout;
use super::OperationDirection;
#[cfg(feature = "win32")]
use super::custom_layout::Column;
#[cfg(feature = "win32")]
use super::custom_layout::ColumnSplit;
#[cfg(feature = "win32")]
use super::custom_layout::ColumnSplitWithCapacity;
#[cfg(feature = "win32")]
use super::custom_layout::CustomLayout;
use crate::default_layout::LayoutOptions;
@@ -404,7 +400,6 @@ fn grid_neighbor(
}
}
#[cfg(feature = "win32")]
impl Direction for CustomLayout {
fn index_in_direction(
&self,

View File

@@ -2,7 +2,6 @@ use serde::Deserialize;
use serde::Serialize;
use super::Arrangement;
#[cfg(feature = "win32")]
use super::CustomLayout;
use super::DefaultLayout;
use super::Direction;
@@ -11,7 +10,6 @@ use super::Direction;
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Layout {
Default(DefaultLayout),
#[cfg(feature = "win32")]
Custom(CustomLayout),
}
@@ -20,7 +18,6 @@ impl Layout {
pub fn as_boxed_direction(&self) -> Box<dyn Direction> {
match self {
Layout::Default(layout) => Box::new(*layout),
#[cfg(feature = "win32")]
Layout::Custom(layout) => Box::new(layout.clone()),
}
}
@@ -29,7 +26,6 @@ impl Layout {
pub fn as_boxed_arrangement(&self) -> Box<dyn Arrangement> {
match self {
Layout::Default(layout) => Box::new(*layout),
#[cfg(feature = "win32")]
Layout::Custom(layout) => Box::new(layout.clone()),
}
}

View File

@@ -1,6 +1,5 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
#![allow(deprecated)] // allow deprecated variants like HidingBehaviour::Hide to be used in derive macros
use std::num::NonZeroUsize;
use std::path::PathBuf;
@@ -15,45 +14,37 @@ use strum::EnumString;
use crate::KomorebiTheme;
use crate::animation::prefix::AnimationPrefix;
use crate::state::State;
// Re-export everything from komorebi-layouts
pub use komorebi_layouts::Arrangement;
pub use komorebi_layouts::Axis;
pub use komorebi_layouts::Column;
pub use komorebi_layouts::ColumnSplit;
pub use komorebi_layouts::ColumnSplitWithCapacity;
pub use komorebi_layouts::ColumnWidth;
pub use komorebi_layouts::CustomLayout;
pub use komorebi_layouts::CycleDirection;
pub use komorebi_layouts::DEFAULT_RATIO;
pub use komorebi_layouts::DEFAULT_SECONDARY_RATIO;
pub use komorebi_layouts::DefaultLayout;
pub use komorebi_layouts::Direction;
pub use komorebi_layouts::GridLayoutOptions;
pub use komorebi_layouts::Layout;
pub use komorebi_layouts::LayoutDefaultEntry;
pub use komorebi_layouts::LayoutOptions;
pub use komorebi_layouts::MAX_RATIO;
pub use komorebi_layouts::MAX_RATIOS;
pub use komorebi_layouts::MIN_RATIO;
pub use komorebi_layouts::OperationDirection;
pub use komorebi_layouts::Rect;
pub use komorebi_layouts::ScrollingLayoutOptions;
pub use komorebi_layouts::Sizing;
pub use komorebi_layouts::validate_ratios;
// Local modules and exports
pub use animation::AnimationStyle;
pub use arrangement::Arrangement;
pub use arrangement::Axis;
pub use custom_layout::Column;
pub use custom_layout::ColumnSplit;
pub use custom_layout::ColumnSplitWithCapacity;
pub use custom_layout::ColumnWidth;
pub use custom_layout::CustomLayout;
pub use cycle_direction::CycleDirection;
pub use default_layout::DefaultLayout;
pub use direction::Direction;
pub use layout::Layout;
pub use operation_direction::OperationDirection;
pub use pathext::PathExt;
pub use pathext::ResolvedPathBuf;
pub use pathext::replace_env_in_path;
pub use pathext::resolve_option_hashmap_usize_path;
pub use rect::Rect;
pub mod animation;
pub mod arrangement;
pub mod asc;
pub mod config_generation;
pub mod custom_layout;
pub mod cycle_direction;
pub mod default_layout;
pub mod direction;
pub mod layout;
pub mod operation_direction;
pub mod pathext;
pub mod rect;
// serde_as must be before derive
#[serde_with::serde_as]
@@ -64,8 +55,6 @@ pub enum SocketMessage {
// Window / Container Commands
FocusWindow(OperationDirection),
MoveWindow(OperationDirection),
PreselectDirection(OperationDirection),
CancelPreselect,
CycleFocusWindow(CycleDirection),
CycleMoveWindow(CycleDirection),
StackWindow(OperationDirection),
@@ -98,7 +87,6 @@ pub enum SocketMessage {
Close,
Minimize,
Promote,
PromoteSwap,
PromoteFocus,
PromoteWindow(OperationDirection),
EagerFocus(String),
@@ -121,7 +109,6 @@ pub enum SocketMessage {
AdjustWorkspacePadding(Sizing, i32),
ChangeLayout(DefaultLayout),
CycleLayout(CycleDirection),
LayoutRatios(Option<Vec<f32>>, Option<Vec<f32>>),
ScrollingLayoutColumns(NonZeroUsize),
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
FlipLayout(Axis),
@@ -258,8 +245,6 @@ pub enum SocketMessage {
StaticConfigSchema,
GenerateStaticConfig,
DebugWindow(isize),
// low level commands
ApplyState(State),
}
impl SocketMessage {
@@ -285,24 +270,17 @@ pub struct SubscribeOptions {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Stackbar mode
pub enum StackbarMode {
/// Always show
Always,
/// Never show
Never,
/// Show on stack
OnStack,
}
#[derive(Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Starbar label
pub enum StackbarLabel {
#[default]
/// Process name
Process,
/// Window title
Title,
}
@@ -310,7 +288,6 @@ pub enum StackbarLabel {
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Border style
pub enum BorderStyle {
#[default]
/// Use the system border style
@@ -325,7 +302,6 @@ pub enum BorderStyle {
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Border style
pub enum BorderImplementation {
#[default]
/// Use the adjustable komorebi border implementation
@@ -349,20 +325,13 @@ pub enum BorderImplementation {
Hash,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Window kind
pub enum WindowKind {
/// Single window
Single,
/// Stack container
Stack,
/// Monocle container
Monocle,
#[default]
/// Unfocused window
Unfocused,
/// Unfocused locked container
UnfocusedLocked,
/// Floating window
Floating,
}
@@ -383,37 +352,30 @@ pub enum StateQuery {
Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Application identifier
pub enum ApplicationIdentifier {
/// Executable name
#[serde(alias = "exe")]
Exe,
/// Class
#[serde(alias = "class")]
Class,
#[serde(alias = "title")]
/// Window title
Title,
/// Executable path
#[serde(alias = "path")]
Path,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Focus follows mouse implementation
pub enum FocusFollowsMouseImplementation {
/// Custom FFM implementation (slightly more CPU-intensive)
/// A custom FFM implementation (slightly more CPU-intensive)
Komorebi,
/// Native (legacy) Windows FFM implementation
/// The native (legacy) Windows FFM implementation
Windows,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Window management behaviour
pub struct WindowManagementBehaviour {
/// The current [`WindowContainerBehaviour`] to be used
/// The current WindowContainerBehaviour to be used
pub current_behaviour: WindowContainerBehaviour,
/// Override of `current_behaviour` to open new windows as floating windows
/// that can be later toggled to tiled, when false it will default to
@@ -432,7 +394,7 @@ pub struct WindowManagementBehaviour {
pub floating_layer_placement: Placement,
/// The `Placement` to be used when spawning a window with float override active
pub float_override_placement: Placement,
/// The `Placement` to be used when spawning a window that matches a `floating_applications` rule
/// The `Placement` to be used when spawning a window that matches a 'floating_applications' rule
pub float_rule_placement: Placement,
}
@@ -440,7 +402,6 @@ pub struct WindowManagementBehaviour {
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Window container behaviour when a new window is opened
pub enum WindowContainerBehaviour {
/// Create a new container for each new window
#[default]
@@ -453,7 +414,6 @@ pub enum WindowContainerBehaviour {
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Floating layer behaviour when a new window is opened
pub enum FloatingLayerBehaviour {
/// Tile new windows (unless they match a float rule or float override is active)
#[default]
@@ -466,7 +426,6 @@ pub enum FloatingLayerBehaviour {
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Placement behaviour for floating windows
pub enum Placement {
/// Does not change the size or position of the window
#[default]
@@ -506,7 +465,6 @@ impl Placement {
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Move behaviour when the operation works across a monitor boundary
pub enum MoveBehaviour {
/// Swap the window container with the window container at the edge of the adjacent monitor
#[default]
@@ -521,7 +479,6 @@ pub enum MoveBehaviour {
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Behaviour when an action would cross a monitor boundary
pub enum CrossBoundaryBehaviour {
/// Attempt to perform actions across a workspace boundary
Workspace,
@@ -532,12 +489,10 @@ pub enum CrossBoundaryBehaviour {
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Window hiding behaviour
pub enum HidingBehaviour {
/// END OF LIFE FEATURE: Use the `SW_HIDE` flag to hide windows when switching workspaces (has issues with Electron apps)
#[deprecated(note = "End of life feature")]
/// END OF LIFE FEATURE: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
Hide,
/// Use the `SW_MINIMIZE` flag to hide windows when switching workspaces (has issues with frequent workspace switching)
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
Minimize,
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
Cloak,
@@ -547,25 +502,44 @@ pub enum HidingBehaviour {
Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Operation behaviour for temporarily unmanaged and floating windows
pub enum OperationBehaviour {
/// Process commands on temporarily unmanaged/floated windows
/// Process komorebic commands on temporarily unmanaged/floated windows
#[default]
Op,
/// Ignore commands on temporarily unmanaged/floated windows
/// Ignore komorebic commands on temporarily unmanaged/floated windows
NoOp,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Sizing {
Increase,
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
}
}
}
}
}
#[derive(
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Window handling behaviour
pub enum WindowHandlingBehaviour {
#[default]
/// Synchronous
Sync,
/// Asynchronous
Async,
}

View File

@@ -121,16 +121,13 @@ impl<'de> serde_with::DeserializeAs<'de, PathBuf> for ResolvedPathBuf {
}
#[cfg(feature = "schemars")]
impl serde_with::schemars_1::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("PathBuf")
impl serde_with::schemars_0_8::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
fn schema_name() -> String {
"PathBuf".to_owned()
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"description": "A file system path. Environment variables like %VAR%, $Env:VAR, or $VAR are automatically resolved."
})
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::schema::Schema {
<PathBuf as schemars::JsonSchema>::json_schema(generator)
}
}

View File

@@ -1,33 +1,20 @@
use serde::Deserialize;
use serde::Serialize;
#[cfg(feature = "win32")]
use windows::Win32::Foundation::RECT;
#[cfg(feature = "darwin")]
use objc2_core_foundation::CGFloat;
#[cfg(feature = "darwin")]
use objc2_core_foundation::CGPoint;
#[cfg(feature = "darwin")]
use objc2_core_foundation::CGRect;
#[cfg(feature = "darwin")]
use objc2_core_foundation::CGSize;
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Rectangle dimensions
pub struct Rect {
/// Left point of the rectangle
/// The left point in a Win32 Rect
pub left: i32,
/// Top point of the rectangle
/// The top point in a Win32 Rect
pub top: i32,
/// Width of the recentangle (from the left point)
/// The right point in a Win32 Rect
pub right: i32,
/// Height of the rectangle (from the top point)
/// The bottom point in a Win32 Rect
pub bottom: i32,
}
#[cfg(feature = "win32")]
impl From<RECT> for Rect {
fn from(rect: RECT) -> Self {
Self {
@@ -39,7 +26,6 @@ impl From<RECT> for Rect {
}
}
#[cfg(feature = "win32")]
impl From<Rect> for RECT {
fn from(rect: Rect) -> Self {
Self {
@@ -51,53 +37,6 @@ impl From<Rect> for RECT {
}
}
#[cfg(feature = "darwin")]
impl From<CGSize> for Rect {
fn from(value: CGSize) -> Self {
Self {
left: 0,
top: 0,
right: value.width as i32,
bottom: value.height as i32,
}
}
}
#[cfg(feature = "darwin")]
impl From<CGRect> for Rect {
fn from(value: CGRect) -> Self {
Self {
left: value.origin.x as i32,
top: value.origin.y as i32,
right: value.size.width as i32,
bottom: value.size.height as i32,
}
}
}
#[cfg(feature = "darwin")]
impl From<&Rect> for CGRect {
fn from(value: &Rect) -> Self {
Self {
origin: CGPoint {
x: value.left as CGFloat,
y: value.top as CGFloat,
},
size: CGSize {
width: value.right as CGFloat,
height: value.bottom as CGFloat,
},
}
}
}
#[cfg(feature = "darwin")]
impl From<Rect> for CGRect {
fn from(value: Rect) -> Self {
CGRect::from(&value)
}
}
impl Rect {
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
self.right == rhs.right && self.bottom == rhs.bottom
@@ -156,7 +95,6 @@ impl Rect {
}
}
#[cfg(feature = "win32")]
#[must_use]
pub const fn rect(&self) -> RECT {
RECT {
@@ -166,19 +104,4 @@ impl Rect {
bottom: self.top + self.bottom,
}
}
#[cfg(feature = "darwin")]
#[must_use]
pub fn percentage_within_horizontal_bounds(&self, other: &Rect) -> f64 {
let overlap_left = self.left.max(other.left);
let overlap_right = (self.left + self.right).min(other.left + other.right);
let overlap_width = overlap_right - overlap_left;
if overlap_width <= 0 {
0.0
} else {
(overlap_width as f64) / (other.right as f64) * 100.0
}
}
}

View File

@@ -238,16 +238,11 @@ lazy_static! {
static ref FLOATING_WINDOW_TOGGLE_ASPECT_RATIO: Arc<Mutex<AspectRatio>> = Arc::new(Mutex::new(AspectRatio::Predefined(PredefinedAspectRatio::Widescreen)));
static ref CURRENT_VIRTUAL_DESKTOP: Arc<Mutex<Option<Vec<u8>>>> = Arc::new(Mutex::new(None));
pub static ref LAYOUT_DEFAULTS: Arc<Mutex<HashMap<DefaultLayout, LayoutDefaultEntry>>> =
Arc::new(Mutex::new(HashMap::new()));
}
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(10);
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(10);
pub static DEFAULT_RESIZE_DELTA: i32 = 50;
pub static DEFAULT_MOUSE_FOLLOWS_FOCUS: bool = true;
pub static INITIAL_CONFIGURATION_LOADED: AtomicBool = AtomicBool::new(false);
pub static CUSTOM_FFM: AtomicBool = AtomicBool::new(false);
pub static SESSION_ID: AtomicU32 = AtomicU32::new(0);
@@ -325,7 +320,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
// the latter case, if the user desires this validation after initiating the task view, komorebi
// should be restarted, and then when this // fn runs again for the first time, it will pick up
// the value of CurrentVirtualDesktop and validate against it accordingly
current.map(|current| current.to_vec())
current
}
#[derive(Clone, Debug, Serialize, Deserialize)]

View File

@@ -176,7 +176,7 @@ struct Opts {
/// Allow the use of komorebi's custom focus-follows-mouse implementation
#[clap(short, long = "ffm")]
focus_follows_mouse: bool,
/// Wait for `komorebic complete-configuration` to be sent before processing events
/// Wait for 'komorebic complete-configuration' to be sent before processing events
#[clap(short, long)]
await_configuration: bool,
/// Start a TCP server on the given port to allow the direct sending of SocketMessages

View File

@@ -307,10 +307,12 @@ impl Monitor {
DefaultLayout::RightMainVerticalStack => {
workspace.add_container_to_front(container);
}
DefaultLayout::UltrawideVerticalStack
if workspace.containers().len() == 1 =>
{
workspace.insert_container_at_idx(0, container);
DefaultLayout::UltrawideVerticalStack => {
if workspace.containers().len() == 1 {
workspace.insert_container_at_idx(0, container);
} else {
workspace.add_container_to_back(container);
}
}
_ => {
workspace.add_container_to_back(container);
@@ -330,10 +332,12 @@ impl Monitor {
match layout {
DefaultLayout::RightMainVerticalStack
| DefaultLayout::UltrawideVerticalStack
if workspace.containers().len() == 1 =>
{
workspace.add_container_to_back(container);
| DefaultLayout::UltrawideVerticalStack => {
if workspace.containers().len() == 1 {
workspace.add_container_to_back(container);
} else {
workspace.insert_container_at_idx(target_index, container);
}
}
_ => {
workspace.insert_container_at_idx(target_index, container);

View File

@@ -25,7 +25,6 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI64;
use std::sync::atomic::Ordering;
pub mod hidden;
@@ -45,10 +44,6 @@ pub enum MonitorNotification {
static ACTIVE: AtomicBool = AtomicBool::new(true);
/// Timestamp (epoch millis) of the last DisplayConnectionChange notification.
/// Used to suppress OS-initiated window minimizes during transient display events.
static LAST_DISPLAY_CHANGE_TIMESTAMP: AtomicI64 = AtomicI64::new(0);
static CHANNEL: OnceLock<(Sender<MonitorNotification>, Receiver<MonitorNotification>)> =
OnceLock::new();
@@ -67,40 +62,11 @@ fn event_rx() -> Receiver<MonitorNotification> {
}
pub fn send_notification(notification: MonitorNotification) {
if matches!(
notification,
MonitorNotification::DisplayConnectionChange
| MonitorNotification::ResumingFromSuspendedState
| MonitorNotification::SessionUnlocked
) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
LAST_DISPLAY_CHANGE_TIMESTAMP.store(now, Ordering::SeqCst);
}
if event_tx().try_send(notification).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
/// Returns true if a display connection change event was received within the
/// last `grace_period` duration. This is used by the event processor to avoid
/// treating OS-initiated minimizes (caused by transient monitor disconnects)
/// as user-initiated minimizes.
pub fn display_change_in_progress(grace_period: std::time::Duration) -> bool {
let last = LAST_DISPLAY_CHANGE_TIMESTAMP.load(Ordering::SeqCst);
if last == 0 {
return false;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
(now - last) < grace_period.as_millis() as i64
}
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
let dip = DISPLAY_INDEX_PREFERENCES.read();
let mut dip_ids = dip.values();
@@ -123,41 +89,7 @@ where
F: Fn() -> I + Copy,
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
{
let mut attempts = 0;
let (displays, errors) = loop {
let (displays, errors): (Vec<_>, Vec<_>) = display_provider().partition(Result::is_ok);
if errors.is_empty() {
break (displays, errors);
}
for err in &errors {
if let Err(e) = err {
tracing::warn!(
"enumerating display in reconciliator (attempt {}): {:?}",
attempts + 1,
e
);
}
}
if attempts < 5 {
attempts += 1;
std::thread::sleep(std::time::Duration::from_millis(150));
continue;
}
break (displays, errors);
};
if !errors.is_empty() {
return Err(color_eyre::eyre::eyre!(
"could not successfully enumerate all displays"
));
}
let all_displays = displays.into_iter().map(Result::unwrap).collect::<Vec<_>>();
let all_displays = display_provider().flatten().collect::<Vec<_>>();
let mut serial_id_map = HashMap::new();
@@ -271,8 +203,6 @@ where
border_manager::send_notification(None);
}
// Keep reference to Arc for potential re-locking
let wm_arc = Arc::clone(&wm);
let mut wm = wm.lock();
let initial_state = State::from(wm.as_ref());
@@ -416,180 +346,12 @@ where
continue 'receiver;
}
// Handle potential monitor removal with verification
let attached_devices = if initial_monitor_count > attached_devices.len() {
if initial_monitor_count > attached_devices.len() {
tracing::info!(
"potential monitor removal detected ({initial_monitor_count} vs {}), verifying in 3s",
"monitor count mismatch ({initial_monitor_count} vs {}), removing disconnected monitors",
attached_devices.len()
);
// Release locks before waiting
drop(wm);
drop(monitor_cache);
// Wait 3 seconds for display state to stabilize
std::thread::sleep(std::time::Duration::from_secs(3));
// Re-query the Win32 display APIs
let re_queried_devices = match attached_display_devices(display_provider) {
Ok(devices) => devices,
Err(e) => {
tracing::error!("failed to re-query display devices: {}", e);
continue 'receiver;
}
};
tracing::debug!(
"after verification: wm had {} monitors, initial query found {}, re-query found {}",
initial_monitor_count,
attached_devices.len(),
re_queried_devices.len()
);
// If monitors are back, the removal was transient (spurious event)
// Still try to restore state since windows might have been minimized
if re_queried_devices.len() >= initial_monitor_count {
tracing::info!(
"monitor removal was transient (spurious event), attempting state restoration. Initial: {}, Re-queried: {}",
initial_monitor_count,
re_queried_devices.len()
);
// Re-acquire locks for state restoration
wm = wm_arc.lock();
// Update Win32 data for all monitors
for monitor in wm.monitors_mut() {
for attached in &re_queried_devices {
let serial_number_ids_match =
if let (Some(attached_snid), Some(m_snid)) =
(&attached.serial_number_id, &monitor.serial_number_id)
{
attached_snid.eq(m_snid)
} else {
false
};
if serial_number_ids_match
|| attached.device_id.eq(&monitor.device_id)
{
monitor.id = attached.id;
monitor.device = attached.device.clone();
monitor.device_id = attached.device_id.clone();
monitor.serial_number_id = attached.serial_number_id.clone();
monitor.name = attached.name.clone();
monitor.size = attached.size;
monitor.work_area_size = attached.work_area_size;
}
}
}
// Try to restore windows that might have been minimized
let offset = wm.work_area_offset;
for monitor in wm.monitors_mut() {
let focused_workspace_idx = monitor.focused_workspace_idx();
for (idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate()
{
let is_focused_workspace = idx == focused_workspace_idx;
if is_focused_workspace {
// Restore containers
for container in workspace.containers_mut() {
if let Some(window) = container.focused_window()
&& WindowsApi::is_window(window.hwnd)
{
tracing::debug!(
"restoring window after transient removal: {}",
window.hwnd
);
WindowsApi::restore_window(window.hwnd);
} else if let Some(window) = container.focused_window() {
tracing::debug!(
"skipping restore of invalid window: {}",
window.hwnd
);
}
}
// Restore maximized window
if let Some(window) = &workspace.maximized_window
&& WindowsApi::is_window(window.hwnd)
{
WindowsApi::restore_window(window.hwnd);
}
// Restore monocle container
if let Some(container) = &workspace.monocle_container
&& let Some(window) = container.focused_window()
&& WindowsApi::is_window(window.hwnd)
{
WindowsApi::restore_window(window.hwnd);
}
// Restore floating windows
for window in workspace.floating_windows() {
if WindowsApi::is_window(window.hwnd) {
WindowsApi::restore_window(window.hwnd);
}
}
}
}
monitor.update_focused_workspace(offset)?;
}
border_manager::send_notification(None);
continue 'receiver;
}
// If monitors are still missing, proceed with actual removal logic
tracing::info!(
"verified monitor removal ({initial_monitor_count} vs {}), removing disconnected monitors",
re_queried_devices.len()
);
// Re-acquire locks for removal processing
wm = wm_arc.lock();
monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
// Make sure that in our state any attached displays have the latest Win32 data
// We must do this again because we dropped the lock and are working with new data
for monitor in wm.monitors_mut() {
for attached in &re_queried_devices {
let serial_number_ids_match =
if let (Some(attached_snid), Some(m_snid)) =
(&attached.serial_number_id, &monitor.serial_number_id)
{
attached_snid.eq(m_snid)
} else {
false
};
if serial_number_ids_match || attached.device_id.eq(&monitor.device_id)
{
monitor.id = attached.id;
monitor.device = attached.device.clone();
monitor.device_id = attached.device_id.clone();
monitor.serial_number_id = attached.serial_number_id.clone();
monitor.name = attached.name.clone();
monitor.size = attached.size;
monitor.work_area_size = attached.work_area_size;
}
}
}
// Use re-queried devices for remaining logic
re_queried_devices
} else {
attached_devices
};
if initial_monitor_count > attached_devices.len() {
tracing::info!("removing disconnected monitors");
// Windows to remove from `known_hwnds`
let mut windows_to_remove = Vec::new();
@@ -822,9 +584,7 @@ where
}
if is_focused_workspace {
if let Some(window) = container.focused_window()
&& WindowsApi::is_window(window.hwnd)
{
if let Some(window) = container.focused_window() {
tracing::debug!(
"restoring window: {}",
window.hwnd
@@ -836,9 +596,7 @@ where
// first window and show that one
container.focus_window(0);
if let Some(window) = container.focused_window()
&& WindowsApi::is_window(window.hwnd)
{
if let Some(window) = container.focused_window() {
WindowsApi::restore_window(window.hwnd);
}
}
@@ -859,9 +617,7 @@ where
|| known_hwnds.contains_key(&window.hwnd)
{
workspace.maximized_window = None;
} else if is_focused_workspace
&& WindowsApi::is_window(window.hwnd)
{
} else if is_focused_workspace {
WindowsApi::restore_window(window.hwnd);
}
}
@@ -875,9 +631,7 @@ where
if container.windows().is_empty() {
workspace.monocle_container = None;
} else if is_focused_workspace {
if let Some(window) = container.focused_window()
&& WindowsApi::is_window(window.hwnd)
{
if let Some(window) = container.focused_window() {
WindowsApi::restore_window(window.hwnd);
} else {
// If the focused window was moved or removed by
@@ -885,9 +639,7 @@ where
// first window and show that one
container.focus_window(0);
if let Some(window) = container.focused_window()
&& WindowsApi::is_window(window.hwnd)
{
if let Some(window) = container.focused_window() {
WindowsApi::restore_window(window.hwnd);
}
}
@@ -901,9 +653,7 @@ where
if is_focused_workspace {
for window in workspace.floating_windows() {
if WindowsApi::is_window(window.hwnd) {
WindowsApi::restore_window(window.hwnd);
}
WindowsApi::restore_window(window.hwnd);
}
}

View File

@@ -60,11 +60,9 @@ use crate::core::Axis;
use crate::core::BorderImplementation;
use crate::core::FocusFollowsMouseImplementation;
use crate::core::Layout;
use crate::core::LayoutOptions;
use crate::core::MoveBehaviour;
use crate::core::OperationDirection;
use crate::core::Rect;
use crate::core::ScrollingLayoutOptions;
use crate::core::Sizing;
use crate::core::SocketMessage;
use crate::core::StateQuery;
@@ -74,6 +72,8 @@ use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::current_virtual_desktop;
use crate::default_layout::LayoutOptions;
use crate::default_layout::ScrollingLayoutOptions;
use crate::monitor::MonitorInformation;
use crate::notify_subscribers;
use crate::stackbar_manager;
@@ -213,7 +213,6 @@ impl WindowManager {
let mut force_update_borders = false;
match message {
SocketMessage::Promote => self.promote_container_to_front()?,
SocketMessage::PromoteSwap => self.promote_container_swap()?,
SocketMessage::PromoteFocus => self.promote_focus_to_front()?,
SocketMessage::PromoteWindow(direction) => {
self.focus_container_in_direction(direction)?;
@@ -304,28 +303,6 @@ impl WindowManager {
}
}
}
SocketMessage::PreselectDirection(direction) => {
let focused_workspace = self.focused_workspace()?;
let mut update = false;
if focused_workspace.preselected_container_idx.is_some() {
tracing::warn!(
"ignoring command as this workspace already has a direction preselect set"
);
} else if matches!(focused_workspace.layer, WorkspaceLayer::Tiling) {
self.preselect_container_in_direction(direction)?;
update = true;
}
if update {
self.focused_workspace_mut()?.update()?;
}
}
SocketMessage::CancelPreselect => {
let focused_workspace = self.focused_workspace_mut()?;
focused_workspace.cancel_preselect();
focused_workspace.update()?;
}
SocketMessage::MoveWindow(direction) => {
let focused_workspace = self.focused_workspace()?;
match focused_workspace.layer {
@@ -947,8 +924,6 @@ impl WindowManager {
center_focused_column: Default::default(),
}),
grid: None,
column_ratios: None,
row_ratios: None,
},
};
@@ -957,29 +932,6 @@ impl WindowManager {
}
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
SocketMessage::LayoutRatios(ref columns, ref rows) => {
use crate::core::validate_ratios;
let focused_workspace = self.focused_workspace_mut()?;
let mut options = focused_workspace.layout_options.unwrap_or(LayoutOptions {
scrolling: None,
grid: None,
column_ratios: None,
row_ratios: None,
});
if let Some(cols) = columns {
options.column_ratios = Some(validate_ratios(cols));
}
if let Some(rws) = rows {
options.row_ratios = Some(validate_ratios(rws));
}
focused_workspace.layout_options = Some(options);
self.update_focused_workspace(false, false)?;
}
SocketMessage::ChangeLayoutCustom(ref path) => {
self.change_workspace_custom_layout(path)?;
}
@@ -2249,7 +2201,14 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
SocketMessage::StaticConfigSchema => {
#[cfg(feature = "schemars")]
{
let socket_message = schemars::schema_for!(SocketMessage);
let settings = schemars::r#gen::SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let generator = settings.into_generator();
let socket_message = generator.into_root_schema_for::<StaticConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
reply.write_all(schema.as_bytes())?;
@@ -2296,9 +2255,6 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
SocketMessage::Theme(ref theme) => {
theme_manager::send_notification(*theme.clone());
}
SocketMessage::ApplyState(ref state) => {
self.apply_state(state.clone());
}
// Deprecated commands
SocketMessage::AltFocusHack(_)
| SocketMessage::IdentifyBorderOverflowApplication(_, _) => {}

View File

@@ -52,9 +52,7 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
if mdm && splash::should().map(|f| f.into()).unwrap_or(true) {
let mut args = vec!["splash".to_string()];
if let Some(server) = server {
if !server.trim().is_empty() {
args.push(server);
}
args.push(server);
}
let _ = Command::new("komorebic").args(&args).spawn();
@@ -266,33 +264,18 @@ impl WindowManager {
}
}
WindowManagerEvent::Minimize(_, window) => {
// During transient display connection changes (e.g. monitor
// briefly disconnecting and reconnecting), Windows may fire
// SystemMinimizeStart for windows on the affected monitor.
// We must not treat these OS-initiated minimizes as user
// actions, otherwise the window gets removed from the
// workspace and the reconciliator cannot restore it.
if crate::monitor_reconciliator::display_change_in_progress(
std::time::Duration::from_secs(10),
) {
tracing::debug!(
"ignoring minimize during display connection change for hwnd: {}",
window.hwnd
);
} else {
let mut hide = false;
let mut hide = false;
{
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if !programmatically_hidden_hwnds.contains(&window.hwnd) {
hide = true;
}
{
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if !programmatically_hidden_hwnds.contains(&window.hwnd) {
hide = true;
}
}
if hide {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false, false)?;
}
if hide {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false, false)?;
}
}
WindowManagerEvent::Hide(_, window) => {
@@ -446,24 +429,6 @@ impl WindowManager {
proceed = false;
}
// after enforce_workspace_rules() has run, check if window exists in ANY workspace
// to prevent duplication when workspace rules move windows across workspaces
if proceed {
let window_already_managed = self
.monitors()
.iter()
.flat_map(|m| m.workspaces())
.any(|ws| ws.contains_window(window.hwnd));
if window_already_managed {
tracing::debug!(
"skipping window addition, already managed after workspace rule enforcement"
);
proceed = false;
}
}
if proceed {
let behaviour = self.window_management_behaviour(
focused_monitor_idx,

View File

@@ -28,10 +28,12 @@ pub fn listen_for_movements(wm: Arc<Mutex<WindowManager>>) {
Action::Press => ignore_movement = true,
Action::Release => ignore_movement = false,
},
Event::MouseMoveRelative { .. } if !ignore_movement => {
match wm.lock().raise_window_at_cursor_pos() {
Ok(()) => {}
Err(error) => tracing::error!("{}", error),
Event::MouseMoveRelative { .. } => {
if !ignore_movement {
match wm.lock().raise_window_at_cursor_pos() {
Ok(()) => {}
Err(error) => tracing::error!("{}", error),
}
}
}
_ => {}

View File

@@ -183,7 +183,6 @@ fn find_orphans() -> color_eyre::Result<()> {
for (hwnd, (m_idx, w_idx)) in cache.iter() {
let window = Window::from(*hwnd);
#[allow(deprecated)]
if !window.is_window()
|| (
// This one is a hack because WINWORD.EXE is an absolute trainwreck of an app

View File

@@ -18,23 +18,20 @@ pub fn mdm_enrollment() -> eyre::Result<(bool, Option<String>)> {
command.args(["/status"]);
let stdout = command.output()?.stdout;
let output = std::str::from_utf8(&stdout)?;
if !output.contains("WorkspaceTenantName") {
if !output.contains("MdmUrl") {
return Ok((false, None));
}
let mut tenant = None;
let mut server = None;
for line in output.lines() {
if line.contains("WorkspaceTenantName") {
if line.contains("MdmUrl") {
let line = line.trim().to_string();
tenant = Some(
line.trim_start_matches("WorkspaceTenantName : ")
.to_string(),
)
server = Some(line.trim_start_matches("MdmUrl : ").to_string())
}
}
Ok((true, tenant))
Ok((true, server))
}
fn is_valid_payload(raw: &str, fresh: bool) -> eyre::Result<bool> {

View File

@@ -253,9 +253,6 @@ impl From<&WindowManager> for State {
layout: workspace.layout.clone(),
layout_options: workspace.layout_options,
layout_rules: workspace.layout_rules.clone(),
layout_options_rules: workspace.layout_options_rules.clone(),
layout_defaults_cache: workspace.layout_defaults_cache.clone(),
work_area_offset_rules: workspace.work_area_offset_rules.clone(),
layout_flip: workspace.layout_flip,
workspace_padding: workspace.workspace_padding,
container_padding: workspace.container_padding,
@@ -275,8 +272,6 @@ impl From<&WindowManager> for State {
globals: workspace.globals,
wallpaper: workspace.wallpaper.clone(),
workspace_config: None,
preselected_container_idx: None,
promotion_swap_container_idx: None,
})
.collect::<VecDeque<_>>();
ws.focus(monitor.workspaces.focused_idx());

View File

@@ -3,8 +3,6 @@ use crate::Axis;
use crate::CrossBoundaryBehaviour;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_MOUSE_FOLLOWS_FOCUS;
use crate::DEFAULT_RESIZE_DELTA;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOATING_APPLICATIONS;
@@ -13,7 +11,6 @@ use crate::FloatingLayerBehaviour;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::LAYOUT_DEFAULTS;
use crate::MANAGE_IDENTIFIERS;
use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
@@ -39,8 +36,6 @@ use crate::animation::ANIMATION_FPS;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::animation::DEFAULT_ANIMATION_FPS;
use crate::animation::DEFAULT_GHOST_MOVEMENT;
use crate::animation::GHOST_MOVEMENT_ENABLED;
use crate::animation::PerAnimationPrefixConfig;
use crate::asc::ApplicationSpecificConfiguration;
use crate::asc::AscApplicationRulesOrSchema;
@@ -56,8 +51,6 @@ use crate::core::DefaultLayout;
use crate::core::FocusFollowsMouseImplementation;
use crate::core::HidingBehaviour;
use crate::core::Layout;
use crate::core::LayoutDefaultEntry;
use crate::core::LayoutOptions;
use crate::core::MoveBehaviour;
use crate::core::OperationBehaviour;
use crate::core::Rect;
@@ -72,6 +65,7 @@ use crate::core::config_generation::ApplicationOptions;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::current_virtual_desktop;
use crate::default_layout::LayoutOptions;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator;
@@ -114,7 +108,6 @@ use uds_windows::UnixStream;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Border colours for different container states
pub struct BorderColours {
/// Border colour when the container contains a single window
#[serde(skip_serializing_if = "Option::is_none")]
@@ -138,69 +131,54 @@ pub struct BorderColours {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Theme options
pub struct ThemeOptions {
/// Specify Light or Dark variant for theme generation
/// Specify Light or Dark variant for theme generation (default: Dark)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::ThemeVariant::Dark)))]
pub theme_variant: Option<komorebi_themes::ThemeVariant>,
/// Border colour when the container contains a single window
/// Border colour when the container contains a single window (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base0D)))]
pub single_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container contains multiple windows
/// Border colour when the container contains multiple windows (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base0B)))]
pub stack_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is in monocle mode
/// Border colour when the container is in monocle mode (default: Base0F)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base0F)))]
pub monocle_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the window is floating
/// Border colour when the window is floating (default: Base09)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base09)))]
pub floating_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused
/// Border colour when the container is unfocused (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base01)))]
pub unfocused_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused and locked
/// Border colour when the container is unfocused and locked (default: Base08)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base08)))]
pub unfocused_locked_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour
/// Stackbar focused tab text colour (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base0B)))]
pub stackbar_focused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar unfocused tab text colour
/// Stackbar unfocused tab text colour (default: Base05)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base05)))]
pub stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar tab background colour
/// Stackbar tab background colour (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base01)))]
pub stackbar_background: Option<komorebi_themes::Base16Value>,
/// Komorebi status bar accent
/// Komorebi status bar accent (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base0D)))]
pub bar_accent: Option<komorebi_themes::Base16Value>,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Wallpaper configuration
pub struct Wallpaper {
/// Path to the wallpaper image file
#[serde_as(as = "ResolvedPathBuf")]
pub path: PathBuf,
/// Generate and apply Base16 theme for this wallpaper
/// Generate and apply Base16 theme for this wallpaper (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
pub generate_theme: Option<bool>,
/// Specify Light or Dark variant for theme generation
/// Specify Light or Dark variant for theme generation (default: Dark)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::ThemeVariant::Dark)))]
pub theme_options: Option<ThemeOptions>,
}
@@ -208,36 +186,23 @@ pub struct Wallpaper {
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Workspace configuration
pub struct WorkspaceConfig {
/// Name
pub name: String,
/// Layout
/// Layout (default: BSP)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = DefaultLayout::BSP)))]
pub layout: Option<DefaultLayout>,
/// Layout-specific options
/// Layout-specific options (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_options: Option<LayoutOptions>,
/// Threshold-based layout options rules in the format of threshold => options.
/// When container count >= threshold, the highest matching threshold's options
/// fully replace the base `layout_options`.
/// This follows the same threshold logic as `layout_rules`.
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_options_rules: Option<HashMap<usize, LayoutOptions>>,
/// END OF LIFE FEATURE: Custom Layout
#[deprecated(note = "End of life feature")]
/// END OF LIFE FEATURE: Custom Layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<ResolvedPathBuf>")]
pub custom_layout: Option<PathBuf>,
/// Layout rules in the format of threshold => layout
/// Layout rules in the format of threshold => layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
/// Work area offset rules in the format of threshold => Rect (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub work_area_offset_rules: Option<HashMap<usize, Rect>>,
/// END OF LIFE FEATURE: Custom layout rules
#[deprecated(note = "End of life feature")]
/// END OF LIFE FEATURE: Custom layout rules (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "resolve_option_hashmap_usize_path", default)]
pub custom_layout_rules: Option<HashMap<usize, PathBuf>>,
@@ -253,34 +218,29 @@ pub struct WorkspaceConfig {
/// Permanent workspace application rules
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_rules: Option<Vec<MatchingRule>>,
/// Workspace specific work area offset
/// Workspace specific work area offset (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub work_area_offset: Option<Rect>,
/// Apply this monitor's window-based work area offset
/// Apply this monitor's window-based work area offset (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
pub apply_window_based_work_area_offset: Option<bool>,
/// Determine what happens when a new window is opened
/// Determine what happens when a new window is opened (default: Create)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = WindowContainerBehaviour::Create)))]
pub window_container_behaviour: Option<WindowContainerBehaviour>,
/// Window container behaviour rules in the format of threshold => behaviour
/// Window container behaviour rules in the format of threshold => behaviour (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_container_behaviour_rules: Option<HashMap<usize, WindowContainerBehaviour>>,
/// Enable or disable float override, which makes it so every new window opens in floating mode
/// Enable or disable float override, which makes it so every new window opens in floating mode (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
pub float_override: Option<bool>,
/// Enable or disable tiling for the workspace
/// Enable or disable tiling for the workspace (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
pub tile: Option<bool>,
/// Specify an axis on which to flip the selected layout
/// Specify an axis on which to flip the selected layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_flip: Option<Axis>,
/// Determine what happens to a new window when the Floating workspace layer is active
/// Determine what happens to a new window when the Floating workspace layer is active (default: Tile)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = FloatingLayerBehaviour::Tile)))]
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
/// Specify a wallpaper for this workspace
#[serde(skip_serializing_if = "Option::is_none")]
@@ -300,13 +260,6 @@ impl From<&Workspace> for WorkspaceConfig {
}
let layout_rules = (!layout_rules.is_empty()).then_some(layout_rules);
let mut work_area_offset_rules = HashMap::new();
for (threshold, offset) in &value.work_area_offset_rules {
work_area_offset_rules.insert(*threshold, *offset);
}
let work_area_offset_rules =
(!work_area_offset_rules.is_empty()).then_some(work_area_offset_rules);
let mut window_container_behaviour_rules = HashMap::new();
for (threshold, behaviour) in value.window_container_behaviour_rules.iter().flatten() {
window_container_behaviour_rules.insert(*threshold, *behaviour);
@@ -345,25 +298,12 @@ impl From<&Workspace> for WorkspaceConfig {
Layout::Custom(_) => None,
})
.flatten(),
layout_options: {
tracing::debug!(
"Parsing workspace config - layout_options: {:?}",
value.layout_options
);
value.layout_options
},
layout_options_rules: if value.layout_options_rules.is_empty() {
None
} else {
Some(value.layout_options_rules.iter().copied().collect())
},
#[allow(deprecated)]
layout_options: value.layout_options,
custom_layout: value
.workspace_config
.as_ref()
.and_then(|c| c.custom_layout.clone()),
layout_rules,
#[allow(deprecated)]
custom_layout_rules: value
.workspace_config
.as_ref()
@@ -378,7 +318,6 @@ impl From<&Workspace> for WorkspaceConfig {
.workspace_config
.as_ref()
.and_then(|c| c.workspace_rules.clone()),
work_area_offset_rules,
work_area_offset: value.work_area_offset,
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset),
window_container_behaviour: value.window_container_behaviour,
@@ -394,19 +333,17 @@ impl From<&Workspace> for WorkspaceConfig {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Monitor configuration
pub struct MonitorConfig {
/// Workspace configurations
pub workspaces: Vec<WorkspaceConfig>,
/// Monitor-specific work area offset
/// Monitor-specific work area offset (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub work_area_offset: Option<Rect>,
/// Window based work area offset
/// Window based work area offset (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub window_based_work_area_offset: Option<Rect>,
/// Open window limit after which the window based work area offset will no longer be applied
/// Open window limit after which the window based work area offset will no longer be applied (default: 1)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = 1)))]
pub window_based_work_area_offset_limit: Option<isize>,
/// Container padding (default: global)
#[serde(skip_serializing_if = "Option::is_none")]
@@ -417,9 +354,8 @@ pub struct MonitorConfig {
/// Specify a wallpaper for this monitor
#[serde(skip_serializing_if = "Option::is_none")]
pub wallpaper: Option<Wallpaper>,
/// Determine what happens to a new window when the Floating workspace layer is active
/// Determine what happens to a new window when the Floating workspace layer is active (default: Tile)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = FloatingLayerBehaviour::Tile)))]
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
}
@@ -466,21 +402,19 @@ impl From<&Monitor> for MonitorConfig {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Path(s) to application-specific configuration file(s)
pub enum AppSpecificConfigurationPath {
/// A single `applications.json` file
/// A single applications.json file
Single(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
/// Multiple `applications.json` files
/// Multiple applications.json files
Multiple(#[serde_as(as = "Vec<ResolvedPathBuf>")] Vec<PathBuf>),
}
#[serde_with::serde_as]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// The `komorebi.json` static configuration file reference for `v0.1.42`
/// The `komorebi.json` static configuration file reference for `v0.1.39`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[deprecated(note = "No longer required")]
#[serde(skip_serializing_if = "Option::is_none")]
pub invisible_borders: Option<Rect>,
/// DISCOURAGED: Minimum width for a window to be eligible for tiling
@@ -489,127 +423,101 @@ pub struct StaticConfig {
/// DISCOURAGED: Minimum height for a window to be eligible for tiling
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum_window_height: Option<i32>,
/// Delta to resize windows by
/// Delta to resize windows by (default 50)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_RESIZE_DELTA)))]
pub resize_delta: Option<i32>,
/// Determine what happens when a new window is opened
/// Determine what happens when a new window is opened (default: Create)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = WindowContainerBehaviour::Create)))]
pub window_container_behaviour: Option<WindowContainerBehaviour>,
/// Enable or disable float override, which makes it so every new window opens in floating mode
/// (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
pub float_override: Option<bool>,
/// Determines what happens on a new window when on the `FloatingLayer`
/// (default: Tile)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = FloatingLayerBehaviour::Tile)))]
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
/// Determines the placement of a new window when toggling to float
/// Determines the placement of a new window when toggling to float (default: CenterAndResize)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Placement::CenterAndResize)))]
pub toggle_float_placement: Option<Placement>,
/// Determines the `Placement` to be used when spawning a window on the floating layer with the
/// `FloatingLayerBehaviour` set to `FloatingLayerBehaviour::Float`
/// `FloatingLayerBehaviour` set to `FloatingLayerBehaviour::Float` (default: Center)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = Placement::Center)))]
pub floating_layer_placement: Option<Placement>,
/// Determines the `Placement` to be used when spawning a window with float override active
/// (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub float_override_placement: Option<Placement>,
/// Determines the `Placement` to be used when spawning a window that matches a
/// `floating_applications` rule
/// 'floating_applications' rule (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub float_rule_placement: Option<Placement>,
/// Determine what happens when a window is moved across a monitor boundary
/// Determine what happens when a window is moved across a monitor boundary (default: Swap)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = MoveBehaviour::Swap)))]
pub cross_monitor_move_behaviour: Option<MoveBehaviour>,
/// Determine what happens when an action is called on a window at a monitor boundary
/// Determine what happens when an action is called on a window at a monitor boundary (default: Monitor)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = CrossBoundaryBehaviour::Monitor)))]
pub cross_boundary_behaviour: Option<CrossBoundaryBehaviour>,
/// Determine what happens when commands are sent while an unmanaged window is in the foreground
/// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = OperationBehaviour::Op)))]
pub unmanaged_window_operation_behaviour: Option<OperationBehaviour>,
/// END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead
#[deprecated(
note = "End of life feature, use [masir](https://github.com/LGUG2Z/masir) instead"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
/// Enable or disable mouse follows focus
/// Enable or disable mouse follows focus (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_MOUSE_FOLLOWS_FOCUS)))]
pub mouse_follows_focus: Option<bool>,
/// Path to applications.json from komorebi-application-specific-configurations
/// Path to applications.json from komorebi-application-specific-configurations (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub app_specific_configuration_path: Option<AppSpecificConfigurationPath>,
/// Width of window borders
/// Width of the window border (default: 8)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_width")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = border_manager::BORDER_WIDTH)))]
pub border_width: Option<i32>,
/// Offset of window borders
/// Offset of the window border (default: -1)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_offset")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = border_manager::BORDER_OFFSET)))]
pub border_offset: Option<i32>,
/// Display window borders
/// Display an active window border (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = border_manager::BORDER_ENABLED)))]
pub border: Option<bool>,
/// Window border colours for different container types (has no effect if [`theme`] is defined)
/// Active window border colours for different container types
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_colours")]
pub border_colours: Option<BorderColours>,
/// Window border style
/// Active window border style (default: System)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_style")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = BorderStyle::System)))]
pub border_style: Option<BorderStyle>,
/// DEPRECATED from v0.1.31: no longer required
#[deprecated(note = "No longer required")]
#[serde(skip_serializing_if = "Option::is_none")]
pub border_z_order: Option<ZOrder>,
/// Window border implementation
/// Active window border implementation (default: Komorebi)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = BorderImplementation::Komorebi)))]
pub border_implementation: Option<BorderImplementation>,
/// Add transparency to unfocused windows
/// Add transparency to unfocused windows (default: false)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = transparency_manager::TRANSPARENCY_ENABLED)))]
pub transparency: Option<bool>,
/// Alpha value for unfocused window transparency [[0-255]]
/// Alpha value for unfocused window transparency [[0-255]] (default: 200)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = transparency_manager::TRANSPARENCY_ALPHA)))]
pub transparency_alpha: Option<u8>,
/// Individual window transparency ignore rules
#[serde(skip_serializing_if = "Option::is_none")]
pub transparency_ignore_rules: Option<Vec<MatchingRule>>,
/// Global default workspace padding
/// Global default workspace padding (default: 10)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_WORKSPACE_PADDING)))]
pub default_workspace_padding: Option<i32>,
/// Global default container padding
/// Global default container padding (default: 10)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_CONTAINER_PADDING)))]
pub default_container_padding: Option<i32>,
/// Per-layout default options and rules, keyed by layout name.
/// Applied as fallback when a workspace does not define its own layout_options or layout_options_rules.
/// If a workspace defines either setting, all global defaults for that layout are completely replaced.
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_defaults: Option<HashMap<DefaultLayout, LayoutDefaultEntry>>,
/// Monitor and workspace configurations
#[serde(skip_serializing_if = "Option::is_none")]
pub monitors: Option<Vec<MonitorConfig>>,
/// Which Windows signal to use when hiding windows
/// Which Windows signal to use when hiding windows (default: Cloak)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = HidingBehaviour::Cloak)))]
pub window_hiding_behaviour: Option<HidingBehaviour>,
/// Global work area (space used for tiling) offset
/// Global work area (space used for tiling) offset (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub global_work_area_offset: Option<Rect>,
/// Individual window floating rules
@@ -628,13 +536,13 @@ pub struct StaticConfig {
/// Identify tray and multi-window applications
#[serde(skip_serializing_if = "Option::is_none")]
pub tray_and_multi_window_applications: Option<Vec<MatchingRule>>,
/// Identify applications that have the `WS_EX_LAYERED` extended window style
/// Identify applications that have the WS_EX_LAYERED extended window style
#[serde(skip_serializing_if = "Option::is_none")]
pub layered_applications: Option<Vec<MatchingRule>>,
/// Identify applications that send `EVENT_OBJECT_NAMECHANGE` on launch (very rare)
/// Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)
#[serde(skip_serializing_if = "Option::is_none")]
pub object_name_change_applications: Option<Vec<MatchingRule>>,
/// Do not process `EVENT_OBJECT_NAMECHANGE` events as Show events for identified applications matching these title regexes
/// Do not process EVENT_OBJECT_NAMECHANGE events as Show events for identified applications matching these title regexes
#[serde(skip_serializing_if = "Option::is_none")]
pub object_name_change_title_ignore_list: Option<Vec<String>>,
/// Set monitor index preferences
@@ -650,16 +558,13 @@ pub struct StaticConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub animation: Option<AnimationsConfig>,
/// Theme configuration options
///
/// If a theme is specified, `border_colours` will have no effect
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<KomorebiTheme>,
/// Identify applications which are slow to send initial event notifications
#[serde(skip_serializing_if = "Option::is_none")]
pub slow_application_identifiers: Option<Vec<MatchingRule>>,
/// How long to wait when compensating for slow applications, in milliseconds
/// How long to wait when compensating for slow applications, in milliseconds (default: 20)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = SLOW_APPLICATION_COMPENSATION_TIME)))]
pub slow_application_compensation_time: Option<u64>,
/// Komorebi status bar configuration files for multiple instances on different monitors
// this option is a little special because it is only consumed by komorebic
@@ -672,39 +577,137 @@ pub struct StaticConfig {
/// Aspect ratio to resize with when toggling floating mode for a window
#[serde(skip_serializing_if = "Option::is_none")]
pub floating_window_aspect_ratio: Option<AspectRatio>,
/// Which Windows API behaviour to use when manipulating windows
/// Which Windows API behaviour to use when manipulating windows (default: Sync)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = WindowHandlingBehaviour::Sync)))]
pub window_handling_behaviour: Option<WindowHandlingBehaviour>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Animations configuration options
pub struct AnimationsConfig {
/// Enable or disable animations
#[cfg_attr(feature = "schemars", schemars(extend("default" = PerAnimationPrefixConfig::Global(false))))]
/// Enable or disable animations (default: false)
pub enabled: PerAnimationPrefixConfig<bool>,
/// Set the animation duration in ms
/// Set the animation duration in ms (default: 250)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = PerAnimationPrefixConfig::Global(250))))]
pub duration: Option<PerAnimationPrefixConfig<u64>>,
/// Set the animation style
/// Set the animation style (default: Linear)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = PerAnimationPrefixConfig::Global(AnimationStyle::Linear))))]
pub style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
/// Set the animation FPS
/// Set the animation FPS (default: 60)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = ANIMATION_FPS)))]
pub fps: Option<u64>,
/// Render movement animations on a GPU-composited ghost surface (recommended).
/// When false, falls back to the legacy per-frame MoveWindow path.
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
pub ghost_movement: Option<bool>,
}
pub use komorebi_themes::KomorebiTheme;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "palette")]
pub enum KomorebiTheme {
/// A theme from catppuccin-egui
Catppuccin {
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
name: komorebi_themes::Catppuccin,
/// Border colour when the container contains a single window (default: Blue)
#[serde(skip_serializing_if = "Option::is_none")]
single_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container contains multiple windows (default: Green)
#[serde(skip_serializing_if = "Option::is_none")]
stack_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is in monocle mode (default: Pink)
#[serde(skip_serializing_if = "Option::is_none")]
monocle_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the window is floating (default: Yellow)
#[serde(skip_serializing_if = "Option::is_none")]
floating_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is unfocused (default: Base)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is unfocused and locked (default: Red)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_locked_border: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar focused tab text colour (default: Green)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar unfocused tab text colour (default: Text)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_unfocused_text: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar tab background colour (default: Base)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_background: Option<komorebi_themes::CatppuccinValue>,
/// Komorebi status bar accent (default: Blue)
#[serde(skip_serializing_if = "Option::is_none")]
bar_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,
/// Border colour when the container contains a single window (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
single_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container contains multiple windows (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stack_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is in monocle mode (default: Base0F)
#[serde(skip_serializing_if = "Option::is_none")]
monocle_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the window is floating (default: Base09)
#[serde(skip_serializing_if = "Option::is_none")]
floating_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused and locked (default: Base08)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_locked_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar unfocused tab text colour (default: Base05)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar tab background colour (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_background: Option<komorebi_themes::Base16Value>,
/// Komorebi status bar accent (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
bar_accent: Option<komorebi_themes::Base16Value>,
},
/// A custom Base16 theme
Custom {
/// Colours of the custom Base16 theme palette
colours: Box<komorebi_themes::Base16ColourPalette>,
/// Border colour when the container contains a single window (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
single_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container contains multiple windows (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stack_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is in monocle mode (default: Base0F)
#[serde(skip_serializing_if = "Option::is_none")]
monocle_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the window is floating (default: Base09)
#[serde(skip_serializing_if = "Option::is_none")]
floating_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused and locked (default: Base08)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_locked_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar unfocused tab text colour (default: Base05)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar tab background colour (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_background: Option<komorebi_themes::Base16Value>,
/// Komorebi status bar accent (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
bar_accent: Option<komorebi_themes::Base16Value>,
},
}
impl StaticConfig {
pub fn end_of_life(raw: &str) {
@@ -793,7 +796,6 @@ impl StaticConfig {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Stackbar tabs configuration
pub struct TabsConfig {
/// Width of a stackbar tab
#[serde(skip_serializing_if = "Option::is_none")]
@@ -817,7 +819,6 @@ pub struct TabsConfig {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Stackbar configuration
pub struct StackbarConfig {
/// Stackbar height
#[serde(skip_serializing_if = "Option::is_none")]
@@ -825,9 +826,8 @@ pub struct StackbarConfig {
/// Stackbar label
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<StackbarLabel>,
/// Stackbar mode
/// Stackbar mode (default: Never)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schemars", schemars(extend("default" = StackbarMode::Never)))]
pub mode: Option<StackbarMode>,
/// Stackbar tab configuration options
#[serde(skip_serializing_if = "Option::is_none")]
@@ -862,7 +862,6 @@ impl From<&WindowManager> for StaticConfig {
};
Self {
#[allow(deprecated)]
invisible_borders: None,
resize_delta: Option::from(value.resize_delta),
window_container_behaviour: Option::from(
@@ -891,7 +890,6 @@ impl From<&WindowManager> for StaticConfig {
),
minimum_window_height: Some(window::MINIMUM_HEIGHT.load(Ordering::SeqCst)),
minimum_window_width: Some(window::MINIMUM_WIDTH.load(Ordering::SeqCst)),
#[allow(deprecated)]
focus_follows_mouse: value.focus_follows_mouse,
mouse_follows_focus: Option::from(value.mouse_follows_focus),
app_specific_configuration_path: None,
@@ -907,7 +905,6 @@ impl From<&WindowManager> for StaticConfig {
),
transparency_ignore_rules: None,
border_style: Option::from(STYLE.load()),
#[allow(deprecated)]
border_z_order: None,
border_implementation: Option::from(IMPLEMENTATION.load()),
default_workspace_padding: Option::from(
@@ -916,14 +913,6 @@ impl From<&WindowManager> for StaticConfig {
default_container_padding: Option::from(
DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst),
),
layout_defaults: {
let guard = LAYOUT_DEFAULTS.lock();
if guard.is_empty() {
None
} else {
Some(guard.clone())
}
},
monitors: Option::from(monitors),
window_hiding_behaviour: Option::from(*HIDING_BEHAVIOUR.lock()),
global_work_area_offset: value.work_area_offset,
@@ -1029,17 +1018,6 @@ impl StaticConfig {
animations.fps.unwrap_or(DEFAULT_ANIMATION_FPS),
Ordering::SeqCst,
);
let ghost_movement_enabled =
animations.ghost_movement.unwrap_or(DEFAULT_GHOST_MOVEMENT);
GHOST_MOVEMENT_ENABLED.store(ghost_movement_enabled, Ordering::SeqCst);
if ghost_movement_enabled {
// Spawn the ghost owner thread now so the first animation
// doesn't pay the spawn + wndclass-registration cost. Lazy
// guarantee preserved: users who turn ghost_movement off
// never trigger this path, so the thread is never created.
crate::animation::ghost::prewarm();
}
}
if let Some(container) = self.default_container_padding {
@@ -1050,23 +1028,9 @@ impl StaticConfig {
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
}
if let Some(defaults) = &self.layout_defaults {
*LAYOUT_DEFAULTS.lock() = defaults.clone();
} else {
LAYOUT_DEFAULTS.lock().clear();
}
if let Some(border_width) = self.border_width {
border_manager::BORDER_WIDTH.store(border_width, Ordering::SeqCst);
}
if let Some(border_offset) = self.border_offset {
border_manager::BORDER_OFFSET.store(border_offset, Ordering::SeqCst);
}
if let Some(border_enabled) = self.border {
border_manager::BORDER_ENABLED.store(border_enabled, Ordering::SeqCst);
}
border_manager::BORDER_WIDTH.store(self.border_width.unwrap_or(8), Ordering::SeqCst);
border_manager::BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
border_manager::BORDER_ENABLED.store(self.border.unwrap_or(true), Ordering::SeqCst);
if let Some(colours) = &self.border_colours {
if let Some(single) = colours.single {
@@ -1120,14 +1084,10 @@ impl StaticConfig {
border_manager::send_notification(None);
}
if let Some(transparency_enabled) = self.transparency {
transparency_manager::TRANSPARENCY_ENABLED
.store(transparency_enabled, Ordering::SeqCst);
}
if let Some(transparency_alpha) = self.transparency_alpha {
transparency_manager::TRANSPARENCY_ALPHA.store(transparency_alpha, Ordering::SeqCst);
}
transparency_manager::TRANSPARENCY_ENABLED
.store(self.transparency.unwrap_or(false), Ordering::SeqCst);
transparency_manager::TRANSPARENCY_ALPHA
.store(self.transparency_alpha.unwrap_or(200), Ordering::SeqCst);
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
@@ -1351,12 +1311,9 @@ impl StaticConfig {
unmanaged_window_operation_behaviour: value
.unmanaged_window_operation_behaviour
.unwrap_or(OperationBehaviour::Op),
resize_delta: value.resize_delta.unwrap_or(DEFAULT_RESIZE_DELTA),
#[allow(deprecated)]
resize_delta: value.resize_delta.unwrap_or(50),
focus_follows_mouse: value.focus_follows_mouse,
mouse_follows_focus: value
.mouse_follows_focus
.unwrap_or(DEFAULT_MOUSE_FOLLOWS_FOCUS),
mouse_follows_focus: value.mouse_follows_focus.unwrap_or(true),
hotwatch: Hotwatch::new()?,
has_pending_raise_op: false,
pending_move_op: Arc::new(None),
@@ -1365,7 +1322,6 @@ impl StaticConfig {
known_hwnds: HashMap::new(),
};
#[allow(deprecated)]
match value.focus_follows_mouse {
None => WindowsApi::disable_focus_follows_mouse()?,
Some(FocusFollowsMouseImplementation::Windows) => {
@@ -1464,7 +1420,7 @@ impl StaticConfig {
workspace_config.layout = Some(DefaultLayout::Columns);
}
ws.load_static_config(workspace_config, value.layout_defaults.as_ref())?;
ws.load_static_config(workspace_config)?;
}
}
@@ -1547,10 +1503,7 @@ impl StaticConfig {
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(
workspace_config,
value.layout_defaults.as_ref(),
)?;
ws.load_static_config(workspace_config)?;
}
}
@@ -1632,7 +1585,7 @@ impl StaticConfig {
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(workspace_config, value.layout_defaults.as_ref())?;
ws.load_static_config(workspace_config)?;
}
}
@@ -1715,10 +1668,7 @@ impl StaticConfig {
for (j, ws) in m.workspaces_mut().iter_mut().enumerate() {
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
ws.load_static_config(
workspace_config,
value.layout_defaults.as_ref(),
)?;
ws.load_static_config(workspace_config)?;
}
}
@@ -1729,10 +1679,7 @@ impl StaticConfig {
wm.enforce_workspace_rules()?;
if let Some(border_enabled) = value.border {
border_manager::BORDER_ENABLED.store(border_enabled, Ordering::SeqCst);
}
border_manager::BORDER_ENABLED.store(value.border.unwrap_or(true), Ordering::SeqCst);
wm.window_management_behaviour.current_behaviour =
value.window_container_behaviour.unwrap_or_default();
wm.window_management_behaviour.float_override = value.float_override.unwrap_or_default();
@@ -1752,15 +1699,10 @@ impl StaticConfig {
wm.unmanaged_window_operation_behaviour = value
.unmanaged_window_operation_behaviour
.unwrap_or_default();
wm.resize_delta = value.resize_delta.unwrap_or(DEFAULT_RESIZE_DELTA);
wm.mouse_follows_focus = value
.mouse_follows_focus
.unwrap_or(DEFAULT_MOUSE_FOLLOWS_FOCUS);
wm.resize_delta = value.resize_delta.unwrap_or(50);
wm.mouse_follows_focus = value.mouse_follows_focus.unwrap_or(true);
wm.work_area_offset = value.global_work_area_offset;
#[allow(deprecated)]
{
wm.focus_follows_mouse = value.focus_follows_mouse;
}
wm.focus_follows_mouse = value.focus_follows_mouse;
match wm.focus_follows_mouse {
None => WindowsApi::disable_focus_follows_mouse()?,
@@ -1986,7 +1928,7 @@ mod tests {
let docs = vec![
"0.1.20", "0.1.21", "0.1.22", "0.1.23", "0.1.24", "0.1.25", "0.1.26", "0.1.27",
"0.1.28", "0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33", "0.1.34", "0.1.35",
"0.1.36", "0.1.37", "0.1.38", "0.1.39",
"0.1.36", "0.1.37",
];
let mut versions = vec![];
@@ -2027,7 +1969,6 @@ mod tests {
"#;
let config = serde_json::from_str::<WorkspaceConfig>(config).unwrap();
#[allow(deprecated)]
let custom_layout_rules = config.custom_layout_rules.unwrap();
assert_eq!(
@@ -2044,10 +1985,7 @@ mod tests {
"name": "Test",
}
"#;
let config = serde_json::from_str::<WorkspaceConfig>(config).unwrap();
#[allow(deprecated)]
let custom_layout_rules = config.custom_layout_rules;
assert_eq!(custom_layout_rules, None);
assert_eq!(config.custom_layout_rules, None);
}
}

View File

@@ -10,9 +10,6 @@ use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use komorebi_themes::Base16Wrapper;
use komorebi_themes::KomorebiThemeBase16 as Base16;
use komorebi_themes::KomorebiThemeCatppuccin as Catppuccin;
use komorebi_themes::KomorebiThemeCustom as Custom;
use komorebi_themes::colour::Colour;
use std::ops::Deref;
use std::sync::OnceLock;
@@ -87,7 +84,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
stackbar_unfocused_text,
stackbar_background,
) = match theme {
KomorebiTheme::Catppuccin(Catppuccin {
KomorebiTheme::Catppuccin {
name,
single_border,
stack_border,
@@ -99,7 +96,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
stackbar_unfocused_text,
stackbar_background,
..
}) => {
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::CatppuccinValue::Blue)
.color32(name.as_theme());
@@ -148,7 +145,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
stackbar_background,
)
}
KomorebiTheme::Base16(Base16 {
KomorebiTheme::Base16 {
name,
single_border,
stack_border,
@@ -160,7 +157,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
stackbar_unfocused_text,
stackbar_background,
..
}) => {
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::Base16Value::Base0D)
.color32(Base16Wrapper::Base16(*name));
@@ -209,7 +206,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
stackbar_background,
)
}
KomorebiTheme::Custom(Custom {
KomorebiTheme::Custom {
colours,
single_border,
stack_border,
@@ -221,7 +218,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
stackbar_unfocused_text,
stackbar_background,
..
}) => {
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::Base16Value::Base0D)
.color32(Base16Wrapper::Custom(colours.clone()));

View File

@@ -17,6 +17,7 @@ use crate::WindowsApi;
use crate::should_act;
pub static TRANSPARENCY_ENABLED: AtomicBool = AtomicBool::new(false);
pub static TRANSPARENCY_ENABLED_OVERRIDE: AtomicBool = AtomicBool::new(false);
pub static TRANSPARENCY_ALPHA: AtomicU8 = AtomicU8::new(200);
static KNOWN_HWNDS: OnceLock<Mutex<Vec<isize>>> = OnceLock::new();

View File

@@ -20,9 +20,7 @@ use crate::animation::ANIMATION_MANAGER;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::animation::AnimationEngine;
use crate::animation::GHOST_MOVEMENT_ENABLED;
use crate::animation::RenderDispatcher;
use crate::animation::ghost::GhostWindow;
use crate::animation::lerp::Lerp;
use crate::animation::prefix::AnimationPrefix;
use crate::animation::prefix::new_animation_key;
@@ -44,7 +42,6 @@ use crate::windows_api;
use crate::windows_api::WindowsApi;
use color_eyre::eyre;
use crossbeam_utils::atomic::AtomicConsume;
use parking_lot::Mutex;
use regex::Regex;
use serde::Deserialize;
use serde::Serialize;
@@ -55,7 +52,6 @@ use std::convert::TryFrom;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Write as _;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::thread;
@@ -169,18 +165,6 @@ struct MovementRenderDispatcher {
target_rect: Rect,
top: bool,
style: AnimationStyle,
/// Some between successful pre_render and post_render/cleanup_on_cancel when
/// ghost movement is active. None for the legacy code path.
ghost: Mutex<Option<GhostWindow>>,
/// Tracks whether the source has been cloaked so cleanup can uncloak idempotently.
cloaked: AtomicBool,
/// Last lerped logical rect actually applied; used by cleanup_on_cancel to
/// snap the real window to the position the user was last seeing.
last_animated_rect: Mutex<Rect>,
/// True when pre_render successfully repositioned the source to target_rect
/// before registering the thumbnail. In that case post_render must skip
/// the final position_window since the source is already there.
pre_painted: AtomicBool,
}
impl MovementRenderDispatcher {
@@ -199,33 +183,37 @@ impl MovementRenderDispatcher {
target_rect,
top,
style,
ghost: Mutex::new(None),
cloaked: AtomicBool::new(false),
last_animated_rect: Mutex::new(start_rect),
pre_painted: AtomicBool::new(false),
}
}
}
fn use_ghost(&self) -> bool {
GHOST_MOVEMENT_ENABLED.load(Ordering::Relaxed)
impl RenderDispatcher for MovementRenderDispatcher {
fn get_animation_key(&self) -> String {
new_animation_key(MovementRenderDispatcher::PREFIX, self.hwnd.to_string())
}
/// Chromium / Electron windows expose a top-level class beginning with
/// `Chrome_WidgetWin_`. Their renderer pipeline is suspended whenever
/// `NativeWindowOcclusionTrackerWin` reads any non-zero `DWMWA_CLOAKED`
/// state on the HWND, so the pre-paint trick (cloak → SetWindowPos →
/// capture) leaves the DComp swap chain stale and the post-uncloak frame
/// shows half-painted / black regions. For these apps we fall back to
/// capture-at-start: keep the source cloaked at start_rect for the whole
/// animation and only move it to target in post_render, where the
/// uncloak is the visibility flip that wakes Viz back up.
fn source_is_chromium_shell(&self) -> bool {
WindowsApi::real_window_class_w(self.hwnd)
.map(|class| class.starts_with("Chrome_WidgetWin_"))
.unwrap_or(false)
fn pre_render(&self) -> eyre::Result<()> {
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification();
Ok(())
}
fn finalise_managers(&self) {
fn render(&self, progress: f64) -> eyre::Result<()> {
let new_rect = self.start_rect.lerp(self.target_rect, progress, self.style);
// we don't check WINDOW_HANDLING_BEHAVIOUR here because animations
// are always run on a separate thread
WindowsApi::move_window(self.hwnd, &new_rect, false)?;
WindowsApi::invalidate_rect(self.hwnd, None, false);
Ok(())
}
fn post_render(&self) -> eyre::Result<()> {
// we don't add the async_window_pos flag here because animations
// are always run on a separate thread
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false)?;
if ANIMATION_MANAGER
.lock()
.count_in_progress(MovementRenderDispatcher::PREFIX)
@@ -240,202 +228,9 @@ impl MovementRenderDispatcher {
stackbar_manager::send_notification();
transparency_manager::send_notification();
}
}
}
impl RenderDispatcher for MovementRenderDispatcher {
fn get_animation_key(&self) -> String {
new_animation_key(MovementRenderDispatcher::PREFIX, self.hwnd.to_string())
}
fn pre_render(&self) -> eyre::Result<()> {
stackbar_manager::STACKBAR_TEMPORARILY_DISABLED.store(true, Ordering::SeqCst);
stackbar_manager::send_notification();
if self.use_ghost() {
let is_chromium = self.source_is_chromium_shell();
// The ghost host is sized to the LOGICAL rect (visible content
// area). DWM thumbnails capture the source at its
// DWMWA_EXTENDED_FRAME_BOUNDS extents (visible content), not
// GetWindowRect outer extents that include the drop-shadow
// margin. Sizing the host to outer dims would stretch the
// visible-content texture by the shadow ratio.
//
// Place the ghost in z-order immediately above the source so
// multiple simultaneously animating windows (workspace switches,
// layout flips) keep the same relative stacking as their
// sources rather than all piling up at HWND_TOP in creation
// order.
//
// For non-Chromium sources we ALSO pre-position the source to
// target_rect *before* registering the thumbnail, so the
// captured pixels reflect target-dimensioned content. The ghost
// dest then animates start → target with the texture
// downscaling to native 1:1 at the end — crisp final frame
// instead of an upscaled blur. For Chromium we skip pre-paint
// (see `source_is_chromium_shell`).
//
// DwmSetWindowAttribute(DWMWA_CLOAK) is rejected with
// E_ACCESSDENIED for foreign HWNDs; the undocumented
// IApplicationView::SetCloak path used elsewhere does not have
// that restriction.
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 2);
self.cloaked.store(true, Ordering::SeqCst);
if !is_chromium {
if let Err(error) =
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false)
{
tracing::warn!(
"ghost movement: failed to pre-position hwnd {}: {error}",
self.hwnd
);
} else {
// No DwmFlush here. DWM thumbnails are live: once
// registered, the thumbnail surface updates as the
// source paints, so the texture catches up to
// target-dim content within the first frame or two of
// the animation. Skipping the flush avoids a ~16ms
// pre-render stall on every non-Chromium animation.
self.pre_painted.store(true, Ordering::SeqCst);
}
}
match GhostWindow::create(self.hwnd, self.start_rect, Some(self.hwnd)) {
Ok(ghost) => {
*self.ghost.lock() = Some(ghost);
}
Err(error) => {
tracing::warn!(
"ghost movement: failed to create ghost for hwnd {}: {error}; \
uncloaking and falling back to legacy path",
self.hwnd
);
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 0);
self.cloaked.store(false, Ordering::SeqCst);
}
}
}
Ok(())
}
fn render(&self, progress: f64) -> eyre::Result<()> {
let logical = self.start_rect.lerp(self.target_rect, progress, self.style);
*self.last_animated_rect.lock() = logical;
let ghost_active = self.ghost.lock().is_some();
if ghost_active {
if let Some(ghost) = self.ghost.lock().as_ref()
&& let Err(error) = ghost.update_rect(logical)
{
tracing::trace!("ghost update_rect failed: {error}");
}
border_manager::animate_to(self.hwnd, logical);
} else {
// Legacy path: animations always run on a separate thread, so we don't
// gate on WINDOW_HANDLING_BEHAVIOUR here.
WindowsApi::move_window(self.hwnd, &logical, false)?;
WindowsApi::invalidate_rect(self.hwnd, None, false);
}
Ok(())
}
fn post_render(&self) -> eyre::Result<()> {
let used_ghost = self.ghost.lock().is_some();
let pre_painted = self.pre_painted.load(Ordering::SeqCst);
// Final single SetWindowPos. For the pre-paint ghost path the source
// has already been moved to target_rect in pre_render and we skip
// this. For the Chromium ghost path (no pre-paint) the source is
// still cloaked at start_rect and needs to be moved here. For the
// legacy non-ghost path this is the original final reposition.
if !pre_painted {
WindowsApi::position_window(self.hwnd, &self.target_rect, self.top, false)?;
}
// Uncloak BEFORE crossfade so the real window's first post-resize
// frame is being composed underneath the still-visible ghost while
// we fade. This gives Chromium/Electron renderers time to produce a
// CompositorFrame at the new size — the visibility flip from
// cloaked-to-uncloaked is what nudges Viz to resume frame
// production.
if self.cloaked.swap(false, Ordering::SeqCst) {
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 0);
}
if used_ghost {
// Crossfade the ghost out over several DWM frames. This masks the
// texture mismatch (start-dim bitmap stretched vs. crisp
// target-dim repaint) and gives slow-to-repaint apps time to
// present their first post-resize frame before the overlay is
// removed. Mirrors KWin's geometry-effect crossfade.
//
// Ease-in curve (1 - t^3): opacity holds high for most of the
// fade and only drops sharply at the end. The ghost stays
// prominent while the real window's first few frames land
// underneath, so the user perceives a smooth reveal rather than
// a snap.
//
// We call set_opacity directly (synchronous DwmUpdateThumbnailProperties
// on this thread) rather than via the ghost owner channel, so
// each step is guaranteed to be visible before the following
// DwmFlush waits for the next vblank.
if let Some(ghost) = self.ghost.lock().as_ref() {
const FADE_STEPS: u32 = 8;
for step in 1..=FADE_STEPS {
let t = step as f32 / FADE_STEPS as f32;
let progress = t * t * t;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let opacity_u8 = ((1.0 - progress) * 255.0).round().clamp(0.0, 255.0) as u8;
let _ = ghost.set_opacity(opacity_u8);
unsafe {
let _ = windows::Win32::Graphics::Dwm::DwmFlush();
}
}
}
} else {
// Legacy path: still benefit from one DWM frame's wait so the
// app's first post-move paint lands.
unsafe {
let _ = windows::Win32::Graphics::Dwm::DwmFlush();
}
}
if let Some(ghost) = self.ghost.lock().take() {
let _ = ghost.dispose();
}
self.finalise_managers();
Ok(())
}
fn cleanup_on_cancel(&self) {
// Snap the real window to wherever the ghost was last drawn so the next
// dispatcher can capture an accurate start_rect. Then uncloak and tear
// down the ghost. Mirrors post_render but uses last_animated_rect.
let target = *self.last_animated_rect.lock();
if let Err(error) = WindowsApi::position_window(self.hwnd, &target, false, false) {
tracing::warn!(
"ghost movement cancel: failed to snap hwnd {} to last rect: {error}",
self.hwnd
);
}
if self.cloaked.swap(false, Ordering::SeqCst) {
SetCloak(Window { hwnd: self.hwnd }.hwnd(), 1, 0);
}
if let Some(ghost) = self.ghost.lock().take() {
let _ = ghost.dispose();
}
self.finalise_managers();
}
}
struct TransparencyRenderDispatcher {
@@ -507,13 +302,10 @@ impl RenderDispatcher for TransparencyRenderDispatcher {
#[derive(Copy, Clone, Debug, Display, EnumString, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Aspect ratio for temporarily floating windows
pub enum AspectRatio {
/// Predefined aspect ratio
#[cfg_attr(feature = "schemars", schemars(title = "Predefined"))]
/// A predefined aspect ratio
Predefined(PredefinedAspectRatio),
/// Custom W:H aspect ratio
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
/// A custom W:H aspect ratio
Custom(i32, i32),
}
@@ -525,7 +317,6 @@ impl Default for AspectRatio {
#[derive(Copy, Clone, Debug, Default, Display, EnumString, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Predefined aspect ratio
pub enum PredefinedAspectRatio {
/// 21:9
Ultrawide,
@@ -700,8 +491,6 @@ impl Window {
}
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
#[allow(deprecated)]
match *hiding_behaviour {
HidingBehaviour::Hide => WindowsApi::hide_window(self.hwnd),
HidingBehaviour::Minimize => WindowsApi::minimize_window(self.hwnd),
@@ -726,8 +515,6 @@ impl Window {
}
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
#[allow(deprecated)]
match *hiding_behaviour {
HidingBehaviour::Hide | HidingBehaviour::Minimize => {
WindowsApi::restore_window(self.hwnd);

View File

@@ -28,7 +28,6 @@ use crate::animation::AnimationEngine;
use crate::core::Arrangement;
use crate::core::Axis;
use crate::core::BorderImplementation;
use crate::core::CustomLayout;
use crate::core::CycleDirection;
use crate::core::DefaultLayout;
use crate::core::FocusFollowsMouseImplementation;
@@ -41,6 +40,7 @@ use crate::core::Sizing;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowManagementBehaviour;
use crate::core::config_generation::MatchingRule;
use crate::core::custom_layout::CustomLayout;
use crate::CrossBoundaryBehaviour;
use crate::DATA_DIR;
@@ -239,30 +239,21 @@ impl WindowManager {
let mouse_follows_focus = self.mouse_follows_focus;
for (monitor_idx, monitor) in self.monitors_mut().iter_mut().enumerate() {
let mut focused_workspace = 0;
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx) {
monitor
.workspaces_mut()
.resize(state_monitor.workspaces().len(), Workspace::default());
for (workspace_idx, workspace) in
monitor.workspaces_mut().iter_mut().enumerate()
for (workspace_idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate() {
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx)
&& let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
{
if let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
{
// to make sure padding and layout_options changes get applied for users after a quick restart
let container_padding = workspace.container_padding;
let workspace_padding = workspace.workspace_padding;
let layout_options = workspace.layout_options;
// to make sure padding changes get applied for users after a quick restart
let container_padding = workspace.container_padding;
let workspace_padding = workspace.workspace_padding;
*workspace = state_workspace.clone();
*workspace = state_workspace.clone();
workspace.container_padding = container_padding;
workspace.workspace_padding = workspace_padding;
workspace.layout_options = layout_options;
workspace.container_padding = container_padding;
workspace.workspace_padding = workspace_padding;
if state_monitor.focused_workspace_idx() == workspace_idx {
focused_workspace = workspace_idx;
}
if state_monitor.focused_workspace_idx() == workspace_idx {
focused_workspace = workspace_idx;
}
}
}
@@ -2051,53 +2042,6 @@ impl WindowManager {
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn preselect_container_in_direction(
&mut self,
direction: OperationDirection,
) -> eyre::Result<()> {
let workspace = self.focused_workspace_mut()?;
let focused_idx = workspace.focused_container_idx();
if matches!(workspace.layout, Layout::Default(DefaultLayout::Grid)) {
tracing::warn!("preselection is not supported on the grid layout");
return Ok(());
}
tracing::info!("preselecting container");
let new_idx =
if workspace.maximized_window.is_some() || workspace.monocle_container.is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
match new_idx {
Some(new_idx) => {
let adjusted_idx = match direction {
OperationDirection::Left | OperationDirection::Up => {
if focused_idx.abs_diff(new_idx) == 1 {
new_idx + 1
} else {
new_idx
}
}
_ => new_idx,
};
workspace.preselect_container_idx(adjusted_idx);
}
None => {
tracing::debug!(
"this is not a valid preselection direction from the current position"
)
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn focus_container_in_direction(
&mut self,
@@ -2110,19 +2054,12 @@ impl WindowManager {
tracing::info!("focusing container");
if workspace.monocle_container.is_some() {
let cycle_direction = match direction {
OperationDirection::Left | OperationDirection::Down => CycleDirection::Previous,
OperationDirection::Right | OperationDirection::Up => CycleDirection::Next,
let new_idx =
if workspace.maximized_window.is_some() || workspace.monocle_container.is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
return self.cycle_monocle(cycle_direction);
}
let new_idx = if workspace.maximized_window.is_some() {
None
} else {
workspace.new_idx_for_direction(direction)
};
let mut cross_monitor_monocle_or_max = false;
@@ -2745,6 +2682,11 @@ impl WindowManager {
#[tracing::instrument(skip(self))]
pub fn stack_all(&mut self) -> eyre::Result<()> {
if transparency_manager::TRANSPARENCY_ENABLED.load(Ordering::SeqCst) {
transparency_manager::TRANSPARENCY_ENABLED.store(false, Ordering::SeqCst);
transparency_manager::TRANSPARENCY_ENABLED_OVERRIDE.store(true, Ordering::SeqCst);
}
self.handle_unmanaged_window_behaviour()?;
tracing::info!("stacking all windows on workspace");
@@ -2812,6 +2754,11 @@ impl WindowManager {
workspace.focus_container_by_window(hwnd)?;
}
if transparency_manager::TRANSPARENCY_ENABLED_OVERRIDE.load(Ordering::SeqCst) {
transparency_manager::TRANSPARENCY_ENABLED.store(true, Ordering::SeqCst);
transparency_manager::TRANSPARENCY_ENABLED_OVERRIDE.store(false, Ordering::SeqCst);
}
if update_workspace {
self.update_focused_workspace(self.mouse_follows_focus, true)?;
}
@@ -2908,39 +2855,6 @@ impl WindowManager {
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn promote_container_swap(&mut self) -> eyre::Result<()> {
self.handle_unmanaged_window_behaviour()?;
let workspace = self.focused_workspace_mut()?;
let focused_container_idx = workspace.focused_container_idx();
let primary_idx = match &workspace.layout {
Layout::Default(_) => 0,
Layout::Custom(layout) => layout.first_container_idx(
layout
.primary_idx()
.ok_or_eyre("this custom layout does not have a primary column")?,
),
};
if matches!(workspace.layout, Layout::Default(DefaultLayout::Grid)) {
tracing::debug!("ignoring promote-swap command for grid layout");
return Ok(());
}
let primary_tile_is_focused = focused_container_idx == primary_idx;
if primary_tile_is_focused && let Some(swap_idx) = workspace.promotion_swap_container_idx {
workspace.swap_containers(focused_container_idx, swap_idx);
} else {
workspace.promotion_swap_container_idx = Some(focused_container_idx);
workspace.swap_containers(focused_container_idx, primary_idx);
}
self.update_focused_workspace(self.mouse_follows_focus, true)
}
#[tracing::instrument(skip(self))]
pub fn promote_focus_to_front(&mut self) -> eyre::Result<()> {
self.handle_unmanaged_window_behaviour()?;
@@ -3107,27 +3021,6 @@ impl WindowManager {
workspace.reintegrate_monocle_container()
}
#[tracing::instrument(skip(self))]
pub fn cycle_monocle(&mut self, direction: CycleDirection) -> eyre::Result<()> {
tracing::info!("cycling monocle container");
if self.focused_workspace()?.containers().is_empty() {
return Ok(());
}
self.focused_workspace_mut()?
.cycle_monocle_container(direction)?;
for container in self.focused_workspace_mut()?.containers_mut() {
container.hide(None);
}
// borders were getting funny during cycles, can't be bothered to root cause it
border_manager::destroy_all_borders()?;
self.update_focused_workspace(true, true)
}
#[tracing::instrument(skip(self))]
pub fn toggle_maximize(&mut self) -> eyre::Result<()> {
self.handle_unmanaged_window_behaviour()?;
@@ -3376,7 +3269,7 @@ impl WindowManager {
let rules: &mut Vec<(usize, Layout)> = &mut workspace.layout_rules;
rules.retain(|pair| pair.0 != at_container_count);
rules.push((at_container_count, Layout::Default(layout)));
rules.sort_by_key(|a| a.0);
rules.sort_by(|a, b| a.0.cmp(&b.0));
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {
@@ -3419,7 +3312,7 @@ impl WindowManager {
let rules: &mut Vec<(usize, Layout)> = &mut workspace.layout_rules;
rules.retain(|pair| pair.0 != at_container_count);
rules.push((at_container_count, Layout::Custom(layout)));
rules.sort_by_key(|a| a.0);
rules.sort_by(|a, b| a.0.cmp(&b.0));
// If this is the focused workspace on a non-focused screen, let's update it
if focused_monitor_idx != monitor_idx && focused_workspace_idx == workspace_idx {

View File

@@ -10,7 +10,6 @@ use std::mem::size_of;
use std::path::Path;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::Foundation::GetLastError;
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Foundation::HINSTANCE;
use windows::Win32::Foundation::HMODULE;
@@ -18,13 +17,10 @@ use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::POINT;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::SetLastError;
use windows::Win32::Foundation::WIN32_ERROR;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_SHELL;
use windows::Win32::Graphics::Dwm::DWM_THUMBNAIL_PROPERTIES;
use windows::Win32::Graphics::Dwm::DWMWA_BORDER_COLOR;
use windows::Win32::Graphics::Dwm::DWMWA_CLOAKED;
use windows::Win32::Graphics::Dwm::DWMWA_COLOR_NONE;
@@ -33,10 +29,7 @@ use windows::Win32::Graphics::Dwm::DWMWA_WINDOW_CORNER_PREFERENCE;
use windows::Win32::Graphics::Dwm::DWMWCP_ROUND;
use windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE;
use windows::Win32::Graphics::Dwm::DwmGetWindowAttribute;
use windows::Win32::Graphics::Dwm::DwmRegisterThumbnail;
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
use windows::Win32::Graphics::Dwm::DwmUnregisterThumbnail;
use windows::Win32::Graphics::Dwm::DwmUpdateThumbnailProperties;
use windows::Win32::Graphics::Gdi::CreateSolidBrush;
use windows::Win32::Graphics::Gdi::EnumDisplayMonitors;
use windows::Win32::Graphics::Gdi::GetMonitorInfoW;
@@ -148,7 +141,6 @@ use windows::Win32::UI::WindowsAndMessaging::WS_DISABLED;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_NOACTIVATE;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TRANSPARENT;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
use windows::Win32::UI::WindowsAndMessaging::WindowFromPoint;
@@ -919,42 +911,18 @@ impl WindowsApi {
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> eyre::Result<isize> {
// Can return 0, which does not always mean that an error has occurred
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
unsafe {
SetLastError(WIN32_ERROR(0));
let result = GetWindowLongPtrW(hwnd, index);
if result != 0 {
Ok(result)
} else {
let last_error = GetLastError();
if last_error == WIN32_ERROR(0) {
Ok(0)
} else {
Err(std::io::Error::from_raw_os_error(last_error.0 as i32).into())
}
}
}
Result::from(WindowsResult::from(unsafe {
GetWindowLongPtrW(hwnd, index)
}))
}
#[cfg(target_pointer_width = "32")]
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> eyre::Result<i32> {
// Can return 0, which does not always mean that an error has occurred
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
unsafe {
SetLastError(WIN32_ERROR(0));
let result = GetWindowLongPtrW(hwnd, index);
if result != 0 {
Ok(result)
} else {
let last_error = GetLastError();
if last_error == WIN32_ERROR(0) {
Ok(0)
} else {
Err(std::io::Error::from_raw_os_error(last_error.0 as i32).into())
}
}
}
Result::from(WindowsResult::from(unsafe {
GetWindowLongPtrW(hwnd, index)
}))
}
#[cfg(target_pointer_width = "64")]
@@ -1348,41 +1316,6 @@ impl WindowsApi {
}
}
pub fn create_ghost_host_window(name: PCWSTR, instance: isize) -> eyre::Result<isize> {
unsafe {
CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TRANSPARENT,
name,
name,
WS_POPUP,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
None,
None,
Option::from(HINSTANCE(as_ptr!(instance))),
None,
)?
}
.process()
}
pub fn dwm_register_thumbnail(dest_hwnd: isize, src_hwnd: isize) -> eyre::Result<isize> {
Ok(unsafe { DwmRegisterThumbnail(HWND(as_ptr!(dest_hwnd)), HWND(as_ptr!(src_hwnd))) }?)
}
pub fn dwm_update_thumbnail_properties(
hthumb: isize,
props: &DWM_THUMBNAIL_PROPERTIES,
) -> eyre::Result<()> {
unsafe { DwmUpdateThumbnailProperties(hthumb, props) }.map_err(Into::into)
}
pub fn dwm_unregister_thumbnail(hthumb: isize) -> eyre::Result<()> {
unsafe { DwmUnregisterThumbnail(hthumb) }.map_err(Into::into)
}
pub fn create_hidden_window(name: PCWSTR, instance: isize) -> eyre::Result<isize> {
unsafe {
CreateWindowExW(

View File

@@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::collections::VecDeque;
use std::ffi::OsStr;
use std::fmt::Display;
@@ -26,10 +25,9 @@ use crate::core::CustomLayout;
use crate::core::CycleDirection;
use crate::core::DefaultLayout;
use crate::core::Layout;
use crate::core::LayoutDefaultEntry;
use crate::core::LayoutOptions;
use crate::core::OperationDirection;
use crate::core::Rect;
use crate::default_layout::LayoutOptions;
use crate::lockable_sequence::LockableSequence;
use crate::ring::Ring;
use crate::should_act;
@@ -42,7 +40,6 @@ use crate::windows_api::WindowsApi;
use color_eyre::eyre;
use color_eyre::eyre::OptionExt;
use komorebi_themes::Base16ColourPalette;
use komorebi_themes::KomorebiThemeCustom as Custom;
use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixStream;
@@ -63,15 +60,6 @@ pub struct Workspace {
pub layout: Layout,
pub layout_options: Option<LayoutOptions>,
pub layout_rules: Vec<(usize, Layout)>,
/// Threshold-based layout options rules (container_count >= threshold -> use these options).
/// Sorted by threshold ascending at load time.
#[serde(default)]
pub layout_options_rules: Vec<(usize, LayoutOptions)>,
/// Cached per-layout defaults from the global `layout_defaults` config setting.
/// Pre-sorted at config load time; used as fallback when workspace has no overrides.
#[serde(skip)]
pub(crate) layout_defaults_cache: HashMap<DefaultLayout, CachedLayoutDefault>,
pub work_area_offset_rules: Vec<(usize, Rect)>,
pub layout_flip: Option<Axis>,
pub workspace_padding: Option<i32>,
pub container_padding: Option<i32>,
@@ -90,10 +78,6 @@ pub struct Workspace {
pub wallpaper: Option<Wallpaper>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_config: Option<WorkspaceConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preselected_container_idx: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub promotion_swap_container_idx: Option<usize>,
}
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -129,9 +113,6 @@ impl Default for Workspace {
layout: Layout::Default(DefaultLayout::BSP),
layout_options: None,
layout_rules: vec![],
layout_options_rules: vec![],
layout_defaults_cache: HashMap::new(),
work_area_offset_rules: vec![],
layout_flip: None,
workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)),
container_padding: Option::from(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)),
@@ -148,8 +129,6 @@ impl Default for Workspace {
globals: Default::default(),
workspace_config: None,
wallpaper: None,
preselected_container_idx: None,
promotion_swap_container_idx: None,
}
}
}
@@ -177,49 +156,8 @@ pub struct WorkspaceGlobals {
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
/// Cached per-layout default options (pre-sorted rules) derived from the global `layout_defaults`.
pub(crate) struct CachedLayoutDefault {
pub layout_options: Option<LayoutOptions>,
/// Threshold-based rules, sorted by threshold ascending at load time
pub layout_options_rules: Vec<(usize, LayoutOptions)>,
}
/// Convert an optional HashMap of threshold-based layout options rules into a Vec sorted by
/// threshold ascending.
fn sorted_layout_options_rules(
rules: Option<&HashMap<usize, LayoutOptions>>,
) -> Vec<(usize, LayoutOptions)> {
match rules {
Some(rules) => {
let mut sorted: Vec<(usize, LayoutOptions)> =
rules.iter().map(|(t, o)| (*t, *o)).collect();
sorted.sort_by_key(|(t, _)| *t);
sorted
}
None => vec![],
}
}
/// Find the highest matching threshold rule for the given container count.
/// Rules must be sorted by threshold ascending.
fn resolve_threshold_match(
rules: &[(usize, LayoutOptions)],
container_count: usize,
) -> Option<LayoutOptions> {
rules
.iter()
.rev()
.find(|(threshold, _)| container_count >= *threshold)
.map(|(_, opts)| *opts)
}
impl Workspace {
pub fn load_static_config(
&mut self,
config: &WorkspaceConfig,
layout_defaults: Option<&HashMap<DefaultLayout, LayoutDefaultEntry>>,
) -> eyre::Result<()> {
pub fn load_static_config(&mut self, config: &WorkspaceConfig) -> eyre::Result<()> {
self.name = Option::from(config.name.clone());
self.container_padding = config.container_padding;
@@ -230,19 +168,14 @@ impl Workspace {
self.layout = Layout::Default(*layout);
}
#[allow(deprecated)]
if let Some(pathbuf) = &config.custom_layout {
let layout = CustomLayout::from_path(pathbuf)?;
self.layout = Layout::Custom(layout);
}
#[allow(deprecated)]
{
self.tile = !(config.custom_layout.is_none()
&& config.layout.is_none()
&& config.tile.is_none()
self.tile =
!(config.custom_layout.is_none() && config.layout.is_none() && config.tile.is_none()
|| config.tile.is_some_and(|tile| !tile));
}
let mut all_layout_rules = vec![];
if let Some(layout_rules) = &config.layout_rules {
@@ -256,7 +189,6 @@ impl Workspace {
self.layout_rules = all_layout_rules.clone();
#[allow(deprecated)]
if let Some(layout_rules) = &config.custom_layout_rules {
for (count, pathbuf) in layout_rules {
let rule = CustomLayout::from_path(pathbuf)?;
@@ -268,15 +200,6 @@ impl Workspace {
self.layout_rules = all_layout_rules;
}
let mut all_work_area_offset_rules = vec![];
if let Some(work_area_offset_rules) = &config.work_area_offset_rules {
for (count, rect) in work_area_offset_rules {
all_work_area_offset_rules.push((*count, *rect));
}
all_work_area_offset_rules.sort_by_key(|(i, _)| *i);
self.work_area_offset_rules = all_work_area_offset_rules;
}
self.work_area_offset = config.work_area_offset;
self.apply_window_based_work_area_offset =
@@ -304,78 +227,13 @@ impl Workspace {
self.layout_flip = config.layout_flip;
self.floating_layer_behaviour = config.floating_layer_behaviour;
self.wallpaper = config.wallpaper.clone();
// Load layout options directly (LayoutOptions is used in both config and runtime)
self.layout_options = config.layout_options;
// Load threshold-based layout options rules, sorted by threshold ascending
self.layout_options_rules =
sorted_layout_options_rules(config.layout_options_rules.as_ref());
tracing::debug!(
"Workspace '{}' loaded layout_options: {:?}, layout_options_rules: {} entries",
self.name.as_deref().unwrap_or("unnamed"),
self.layout_options,
self.layout_options_rules.len(),
);
// Cache per-layout defaults from global layout_defaults, pre-sorting rules
self.layout_defaults_cache = if let Some(defaults) = layout_defaults {
defaults
.iter()
.map(|(layout, entry)| {
(
*layout,
CachedLayoutDefault {
layout_options: entry.layout_options,
layout_options_rules: sorted_layout_options_rules(
entry.layout_options_rules.as_ref(),
),
},
)
})
.collect()
} else {
HashMap::new()
};
self.workspace_config = Some(config.clone());
Ok(())
}
/// Compute effective layout options using the complete-replacement cascade:
///
/// If the workspace defines EITHER `layout_options` OR `layout_options_rules`,
/// it completely replaces the global `layout_defaults` for this layout.
/// Global defaults are only used when the workspace has NEITHER setting.
///
/// Within the effective source (workspace or global):
/// 1. Try threshold match from rules (highest matching threshold wins)
/// 2. If a rule matches -> use it (full replacement of base)
/// 3. Else -> use the base `layout_options`
fn effective_layout_options(&self) -> Option<LayoutOptions> {
let container_count = self.containers().len();
let has_workspace_overrides =
self.layout_options.is_some() || !self.layout_options_rules.is_empty();
let (effective_base, effective_rules): (Option<LayoutOptions>, &[(usize, LayoutOptions)]) =
if has_workspace_overrides {
(self.layout_options, &self.layout_options_rules)
} else {
match &self.layout {
Layout::Default(dl) => match self.layout_defaults_cache.get(dl) {
Some(entry) => (entry.layout_options, &entry.layout_options_rules),
None => (None, &[]),
},
Layout::Custom(_) => (None, &[]),
}
};
resolve_threshold_match(effective_rules, container_count).or(effective_base)
}
pub fn hide(&mut self, omit: Option<isize>) {
for window in self.floating_windows_mut().iter_mut().rev() {
let mut should_hide = omit.is_none();
@@ -464,7 +322,7 @@ impl Workspace {
}
if let Some(palette) = base16_palette {
let komorebi_theme = KomorebiTheme::Custom(Custom {
let komorebi_theme = KomorebiTheme::Custom {
colours: Box::new(palette),
single_border: wallpaper
.theme_options
@@ -503,7 +361,7 @@ impl Workspace {
.as_ref()
.and_then(|o| o.stackbar_background),
bar_accent: wallpaper.theme_options.as_ref().and_then(|o| o.bar_accent),
});
};
let bytes = SocketMessage::Theme(Box::new(komorebi_theme)).as_bytes()?;
@@ -588,8 +446,7 @@ impl Workspace {
}
// make sure we are never holding on to empty containers
self.containers_mut()
.retain(|c| c.is_preselect() || !c.windows().is_empty());
self.containers_mut().retain(|c| !c.windows().is_empty());
let container_padding = self
.container_padding
@@ -602,27 +459,9 @@ impl Workspace {
let border_width = self.globals.border_width;
let border_offset = self.globals.border_offset;
let work_area = self.globals.work_area;
let work_area_offset = self.work_area_offset.or(self.globals.work_area_offset);
let window_based_work_area_offset = self.globals.window_based_work_area_offset;
let window_based_work_area_offset_limit = self.globals.window_based_work_area_offset_limit;
let mut rules_work_area_offset = None;
if !self.work_area_offset_rules.is_empty() {
let count = if self.monocle_container.is_some() {
1
} else {
self.containers().len()
};
for (threshold, work_area_offset_rule) in &self.work_area_offset_rules {
if count >= *threshold {
rules_work_area_offset = Some(*work_area_offset_rule);
}
}
};
let work_area_offset = rules_work_area_offset
.or(self.work_area_offset)
.or(self.globals.work_area_offset);
let mut adjusted_work_area = work_area_offset.map_or_else(
|| work_area,
@@ -636,6 +475,7 @@ impl Workspace {
with_offset
},
);
if (self.containers().len() <= window_based_work_area_offset_limit as usize
|| self.monocle_container.is_some() && window_based_work_area_offset_limit > 0)
&& self.apply_window_based_work_area_offset
@@ -696,15 +536,6 @@ impl Workspace {
} else if let Some(window) = &mut self.maximized_window {
window.maximize();
} else if !self.containers().is_empty() {
let effective_layout_options = self.effective_layout_options();
tracing::debug!(
"Workspace '{}' update() - effective_layout_options: {:?} (base: {:?}, rules: {})",
self.name.as_deref().unwrap_or("unnamed"),
effective_layout_options,
self.layout_options,
self.layout_options_rules.len(),
);
let mut layouts = self.layout.as_boxed_arrangement().calculate(
&adjusted_work_area,
NonZeroUsize::new(self.containers().len()).ok_or_eyre(
@@ -714,7 +545,7 @@ impl Workspace {
self.layout_flip,
&self.resize_dimensions,
self.focused_container_idx(),
effective_layout_options,
self.layout_options,
&self.latest_layout,
);
@@ -987,10 +818,9 @@ impl Workspace {
}
pub fn promote_container(&mut self) -> eyre::Result<()> {
let focused_idx = self.focused_container_idx();
let resize = self.resize_dimensions.remove(0);
let container = self
.containers_mut()
.remove_respecting_locks(focused_idx)
.remove_focused_container()
.ok_or_eyre("there is no container")?;
let primary_idx = match &self.layout {
@@ -1002,10 +832,9 @@ impl Workspace {
),
};
let insertion_idx = self
.containers_mut()
.insert_respecting_locks(primary_idx, container);
self.focus_container(insertion_idx);
let insertion_idx = self.insert_container_at_idx(primary_idx, container);
self.resize_dimensions[insertion_idx] = resize;
self.focus_container(primary_idx);
Ok(())
}
@@ -1147,18 +976,6 @@ impl Workspace {
container
}
pub fn preselect_container_idx(&mut self, insertion_idx: usize) {
self.preselected_container_idx = Some(insertion_idx);
self.insert_container_at_idx(insertion_idx, Container::preselect());
}
pub fn cancel_preselect(&mut self) {
if let Some(idx) = self.preselected_container_idx {
self.containers_mut().remove_respecting_locks(idx);
self.preselected_container_idx = None;
}
}
pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
let len = NonZeroUsize::new(self.containers().len())?;
@@ -1258,12 +1075,7 @@ impl Workspace {
}
pub fn new_container_for_window(&mut self, window: Window) {
let next_idx = if let Some(idx) = self.preselected_container_idx {
let next = idx;
self.preselected_container_idx = None;
self.remove_container_by_idx(next);
next
} else if self.containers().is_empty() {
let next_idx = if self.containers().is_empty() {
0
} else {
self.focused_container_idx() + 1
@@ -1426,7 +1238,7 @@ impl Workspace {
0 | 1 => self.enforce_no_resize(),
_ => {
// Zero is actually on the left
if let Some(left) = resize_dimensions[0].as_mut() {
if let Some(mut left) = resize_dimensions[0] {
left.top = 0;
left.bottom = 0;
left.left = 0;
@@ -1459,7 +1271,7 @@ impl Workspace {
0 | 1 => self.enforce_no_resize(),
_ => {
// Zero is actually on the right
if let Some(left) = resize_dimensions[1].as_mut() {
if let Some(mut left) = resize_dimensions[1] {
left.top = 0;
left.bottom = 0;
left.right = 0;
@@ -1490,7 +1302,7 @@ impl Workspace {
match resize_dimensions.len() {
0 | 1 => self.enforce_no_resize(),
_ => {
if let Some(left) = resize_dimensions[0].as_mut() {
if let Some(mut left) = resize_dimensions[0] {
left.top = 0;
left.left = 0;
left.right = 0;
@@ -1521,14 +1333,14 @@ impl Workspace {
// Two windows can only be resized in the middle
2 => {
// Zero is actually on the right
if let Some(right) = resize_dimensions[0].as_mut() {
if let Some(mut right) = resize_dimensions[0] {
right.top = 0;
right.bottom = 0;
right.right = 0;
}
// One is on the left
if let Some(left) = resize_dimensions[1].as_mut() {
if let Some(mut left) = resize_dimensions[1] {
left.top = 0;
left.bottom = 0;
left.left = 0;
@@ -1538,13 +1350,13 @@ impl Workspace {
// stack on the right
_ => {
// Central can be resized left or right
if let Some(right) = resize_dimensions[0].as_mut() {
if let Some(mut right) = resize_dimensions[0] {
right.top = 0;
right.bottom = 0;
}
// Left one can only be resized to the right
if let Some(left) = resize_dimensions[1].as_mut() {
if let Some(mut left) = resize_dimensions[1] {
left.top = 0;
left.bottom = 0;
left.left = 0;
@@ -1659,23 +1471,6 @@ impl Workspace {
Ok(())
}
pub fn cycle_monocle_container(&mut self, direction: CycleDirection) -> eyre::Result<()> {
if self.containers().is_empty() {
return Ok(());
}
self.reintegrate_monocle_container()?;
let new_idx = self
.new_idx_for_cycle_direction(direction)
.ok_or_eyre("there is no container to cycle monocle to")?;
self.focus_container(new_idx);
self.new_monocle_container()?;
Ok(())
}
pub fn new_maximized_window(&mut self) -> eyre::Result<()> {
let focused_idx = self.focused_container_idx();

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebic-no-console"
version = "0.1.42"
version = "0.1.39"
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2024"
@@ -8,3 +8,5 @@ edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

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