Compare commits

..

1 Commits

Author SHA1 Message Date
LGUG2Z
e52796c24d wip 2025-05-17 20:43:27 -07:00
126 changed files with 71967 additions and 23241 deletions

1
.envrc
View File

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

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

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Check and close feature issues
uses: actions/github-script@v8
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;

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@v4
with:
fetch-depth: 0
- uses: EmbarkStudios/cargo-deny-action@v2
@@ -43,11 +43,10 @@ jobs:
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: rustup toolchain install stable --profile minimal
- run: rustup component add --toolchain stable-x86_64-pc-windows-msvc clippy
- run: rustup toolchain install nightly --allow-downgrade -c rustfmt
- uses: Swatinem/rust-cache@v2
with:
@@ -65,7 +64,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@v5
- uses: actions/upload-artifact@v4
with:
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
path: |
@@ -82,12 +81,12 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: echo "VERSION=nightly" >> $GITHUB_ENV
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
@@ -129,14 +128,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.event.release.tag_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
@@ -171,14 +170,14 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: |
TAG=${{ github.ref_name }}
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v4
- run: |
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi

6
.gitignore vendored
View File

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

3535
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,14 @@
resolver = "2"
members = [
"komorebi",
"komorebi-client",
"komorebi-gui",
"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]
@@ -19,12 +19,12 @@ chrono = "0.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
color-eyre = "0.6"
eframe = "0.33"
egui_extras = "0.33"
eframe = "0.31"
egui_extras = "0.31"
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" }
@@ -35,53 +35,43 @@ tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
parking_lot = "0.12"
paste = "1"
sysinfo = "0.37"
sysinfo = "0.34"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
windows-numerics = { version = "0.3" }
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "a28c6559a9de2f92c142a714947a9b081776caca" }
windows-numerics = { version = "0.2" }
windows-implement = { version = "0.60" }
windows-interface = { version = "0.59" }
windows-core = { version = "0.62" }
windows-core = { version = "0.61" }
shadow-rs = "1"
which = "8"
which = "7"
[workspace.dependencies.windows]
version = "0.62"
version = "0.61"
features = [
"Foundation_Numerics",
"Win32_Devices",
"Win32_Devices_Display",
"Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader",
"Win32_System_Power",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",
"Win32_UI_Accessibility",
"Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming",
"Media",
"Media_Control",
"Foundation_Numerics",
"Win32_Devices",
"Win32_Devices_Display",
"Win32_System_Com",
"Win32_UI_Shell_Common", # for IObjectArray
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Dxgi_Common",
"Win32_System_LibraryLoader",
"Win32_System_Power",
"Win32_System_RemoteDesktop",
"Win32_System_Threading",
"Win32_UI_Accessibility",
"Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming",
"Media",
"Media_Control"
]
[profile.release-opt]
inherits = "release"
lto = true
panic = "abort"
codegen-units = 1
strip = true
[workspace.metadata.crane]
name = "komorebi-workspace"

View File

@@ -29,46 +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
If you made your way to this repo looking for [komorebi for
Mac](https://github.com/KomoCorp/komorebi-for-mac), the project is currently
being developed in private with [early access available to GitHub
Sponsors](https://github.com/sponsors/LGUG2Z).
If you want to see how far along development is before signing up for early
access (spoiler: it's very far along!) there is an overview video you can watch
[here](https://www.youtube.com/watch?v=u3eJcsa_MJk).
Sponsors with early access can install komorebi for Mac either by compiling
from source, by using Homebrew, or by using the project's Nix Flake.
## Overview
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
@@ -434,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", tag = "v0.1.39"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.37"}
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,29 @@ 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" }
]
[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",
"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
@@ -95,11 +93,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"
@@ -112,12 +105,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/catppuccin-egui",
"https://github.com/LGUG2Z/windows-icons",
"https://github.com/LGUG2Z/win32-display-data",
"https://github.com/LGUG2Z/flavours",
"https://github.com/LGUG2Z/base16_color_scheme",
"https://github.com/LGUG2Z/whkd",
]

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,8 @@ Options:
Desired ease function for animation
[default: linear]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo,
ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint,
ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
-a, --animation-type <ANIMATION_TYPE>
Animation type to apply the style to. If not specified, sets global style

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

@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
Arguments:
<DEFAULT_LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -12,6 +12,9 @@ Options:
--whkd
Enable autostart of whkd
--ahk
Enable autostart of ahk
--bar
Enable autostart of komorebi-bar

View File

@@ -9,6 +9,9 @@ Options:
--whkd
Kill whkd if it is running as a background process
--ahk
Kill ahk if it is running as a background process
--bar
Kill komorebi-bar if it is running as a background process

View File

@@ -1,16 +0,0 @@
# license
```
Specify an email associated with an Individual Commercial Use License
Usage: komorebic.exe license <EMAIL>
Arguments:
<EMAIL>
Email address associated with an Individual Commercial Use License
Options:
-h, --help
Print help
```

View File

@@ -13,7 +13,7 @@ Arguments:
The number of window containers on-screen required to trigger this layout rule
<LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -10,7 +10,7 @@ Arguments:
Target workspace name
<VALUE>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --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,16 +0,0 @@
# scrolling-layout-columns
```
Set the number of visible columns for the Scrolling layout on the focused workspace
Usage: komorebic.exe scrolling-layout-columns <COUNT>
Arguments:
<COUNT>
Desired number of visible columns
Options:
-h, --help
Print help
```

View File

@@ -18,6 +18,9 @@ Options:
--whkd
Start whkd in a background process
--ahk
Start autohotkey configuration file
--bar
Start komorebi-bar in a background process

View File

@@ -9,6 +9,9 @@ Options:
--whkd
Stop whkd if it is running as a background process
--ahk
Stop ahk if it is running as a background process
--bar
Stop komorebi-bar if it is running as a background process

View File

@@ -1,7 +1,7 @@
# toggle-pause
```
Toggle the paused state for all window tiling
Toggle window tiling on the focused workspace
Usage: komorebic.exe toggle-pause

View File

@@ -16,7 +16,7 @@ Arguments:
The number of window containers on-screen required to trigger this layout rule
<LAYOUT>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -13,7 +13,7 @@ Arguments:
Workspace index on the specified monitor (zero-indexed)
<VALUE>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
Options:
-h, --help

View File

@@ -1,31 +0,0 @@
# workspace-work-area-offset
```
Set offsets for a workspace to exclude parts of the work area from tiling
Usage: komorebic.exe workspace-work-area-offset <MONITOR> <WORKSPACE> <LEFT> <TOP> <RIGHT> <BOTTOM>
Arguments:
<MONITOR>
Monitor index (zero-indexed)
<WORKSPACE>
Workspace index (zero-indexed)
<LEFT>
Size of the left work area offset (set right to left * 2 to maintain right padding)
<TOP>
Size of the top work area offset (set bottom to the same value to maintain bottom padding)
<RIGHT>
Size of the right work area offset
<BOTTOM>
Size of the bottom work area offset
Options:
-h, --help
Print help
```

View File

@@ -16,19 +16,6 @@ the example files have been downloaded. For most new users this will be in the
komorebic quickstart
```
## Corporate Devices Enrolled in MDM
If you are using `komorebi` on a corporate device enrolled in mobile device
management, you will receive a pop-up when you run `komorebic start` reminding
you that the [Komorebi License](https://github.com/LGUG2Z/komorebi-license) does
not permit any kind of commercial use.
You can remove this pop-up by running `komorebic license <email>` with the email
associated with your Individual Commercial Use License. A single HTTP request
will be sent with the given email address to verify license validity.
## Starting komorebi
With the example configurations downloaded, you can now start `komorebi`,
`komorebi-bar` and `whkd`.
@@ -36,9 +23,6 @@ With the example configurations downloaded, you can now start `komorebi`,
komorebic start --whkd --bar
```
If you don't want to use the komorebi status bar, you can remove the `--bar` option
from the above command.
## komorebi.json
The example window manager configuration sets some sane defaults and provides

View File

@@ -1,5 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.bar.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.37/schema.bar.json",
"monitor": 0,
"font_family": "JetBrains Mono",
"theme": {
"palette": "Base16",
@@ -47,8 +48,8 @@
{
"Network": {
"enable": true,
"show_activity": true,
"show_total_activity": 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.40/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.37/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

@@ -15,9 +15,6 @@ fmt:
prettier --write .github/FUNDING.yml
prettier --write .github/workflows/windows.yaml
fix:
cargo clippy --fix --allow-dirty
install-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
@@ -75,38 +72,22 @@ 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,7 +1,7 @@
[package]
name = "komorebi-bar"
version = "0.1.40"
edition = "2024"
version = "0.1.38"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -17,12 +17,12 @@ 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-phosphor = "0.9"
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
lazy_static = { workspace = true }
netdev = "0.40"
netdev = "0.34"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
@@ -44,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

@@ -1,28 +1,27 @@
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::BAR_HEIGHT;
use crate::DEFAULT_PADDING;
use crate::KomorebiEvent;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_LEFT;
use crate::MONITOR_RIGHT;
use crate::MONITOR_TOP;
use crate::config::get_individual_spacing;
use crate::config::KomobarConfig;
use crate::config::KomobarTheme;
use crate::config::MonitorConfigOrIndex;
use crate::config::Position;
use crate::config::PositionConfig;
use crate::config::get_individual_spacing;
use crate::process_hwnd;
use crate::render::Color32Ext;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::MonitorInfo;
use crate::widgets::komorebi::KomorebiNotificationState;
use crate::widgets::widget::BarWidget;
use crate::widgets::widget::WidgetConfig;
use color_eyre::eyre;
use crate::KomorebiEvent;
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::BAR_HEIGHT;
use crate::DEFAULT_PADDING;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_LEFT;
use crate::MONITOR_RIGHT;
use crate::MONITOR_TOP;
use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError;
use eframe::egui::Align;
@@ -48,23 +47,24 @@ use eframe::egui::Visuals;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::Colour;
use komorebi_client::KomorebiTheme;
use komorebi_client::MonitorNotification;
use komorebi_client::NotificationEvent;
use komorebi_client::PathExt;
use komorebi_client::SocketMessage;
use komorebi_client::VirtualDesktopNotification;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
use komorebi_themes::Base16Wrapper;
use komorebi_themes::Catppuccin;
use komorebi_themes::KomobarThemeBase16;
use komorebi_themes::KomobarThemeCatppuccin;
use komorebi_themes::KomobarThemeCustom;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::CatppuccinValue;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::Error;
use std::io::ErrorKind;
use std::io::Result;
use std::io::Write;
use std::os::windows::process::CommandExt;
use std::path::PathBuf;
@@ -72,8 +72,8 @@ use std::process::ChildStdin;
use std::process::Command;
use std::process::Stdio;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::sync::Arc;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
@@ -81,7 +81,7 @@ lazy_static! {
static ref SESSION_STDIN: Mutex<Option<ChildStdin>> = Mutex::new(None);
}
fn start_powershell() -> eyre::Result<()> {
fn start_powershell() -> Result<()> {
// found running session, do nothing
if SESSION_STDIN.lock().as_mut().is_some() {
tracing::debug!("PowerShell session already started");
@@ -105,17 +105,17 @@ fn start_powershell() -> eyre::Result<()> {
Ok(())
}
fn stop_powershell() -> eyre::Result<()> {
fn stop_powershell() -> Result<()> {
tracing::debug!("Stopping PowerShell session");
if let Some(mut session_stdin) = SESSION_STDIN.lock().take() {
if let Err(e) = session_stdin.write_all(b"exit\n") {
tracing::error!(error = %e, "failed to write exit command to PowerShell stdin");
return Err(e.into());
return Err(e);
}
if let Err(e) = session_stdin.flush() {
tracing::error!(error = %e, "failed to flush PowerShell stdin");
return Err(e.into());
return Err(e);
}
tracing::debug!("PowerShell session stopped");
@@ -126,22 +126,25 @@ fn stop_powershell() -> eyre::Result<()> {
Ok(())
}
pub fn exec_powershell(cmd: &str) -> eyre::Result<()> {
pub fn exec_powershell(cmd: &str) -> Result<()> {
if let Some(session_stdin) = SESSION_STDIN.lock().as_mut() {
if let Err(e) = writeln!(session_stdin, "{cmd}") {
if let Err(e) = writeln!(session_stdin, "{}", cmd) {
tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin");
return Err(e.into());
return Err(e);
}
if let Err(e) = session_stdin.flush() {
tracing::error!(error = %e, "failed to flush PowerShell stdin");
return Err(e.into());
return Err(e);
}
return Ok(());
}
Err(Error::new(ErrorKind::NotFound, "PowerShell session not started").into())
Err(Error::new(
ErrorKind::NotFound,
"PowerShell session not started",
))
}
pub struct Komobar {
@@ -150,7 +153,7 @@ pub struct Komobar {
pub disabled: bool,
pub config: KomobarConfig,
pub render_config: Rc<RefCell<RenderConfig>>,
pub monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
pub center_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>,
@@ -186,12 +189,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);
@@ -256,12 +259,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));
@@ -279,12 +282,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();
@@ -322,15 +325,16 @@ pub fn apply_theme(
// apply rounding to the widgets
if let Some(Grouping::Bar(config) | Grouping::Alignment(config) | Grouping::Widget(config)) =
&grouping
&& let Some(rounding) = config.rounding
{
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
style.visuals.widgets.inactive.corner_radius = rounding.into();
style.visuals.widgets.hovered.corner_radius = rounding.into();
style.visuals.widgets.active.corner_radius = rounding.into();
style.visuals.widgets.open.corner_radius = rounding.into();
});
if let Some(rounding) = config.rounding {
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.corner_radius = rounding.into();
style.visuals.widgets.inactive.corner_radius = rounding.into();
style.visuals.widgets.hovered.corner_radius = rounding.into();
style.visuals.widgets.active.corner_radius = rounding.into();
style.visuals.widgets.open.corner_radius = rounding.into();
});
}
}
// Update RenderConfig's background_color so that widgets will have the new color
@@ -341,7 +345,7 @@ impl Komobar {
pub fn apply_config(
&mut self,
ctx: &Context,
previous_monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
) {
MAX_LABEL_WIDTH.store(
self.config.max_label_width.unwrap_or(400.0) as i32,
@@ -370,7 +374,7 @@ impl Komobar {
self.config.icon_scale,
));
let mut monitor_info = previous_monitor_info;
let mut komorebi_notification_state = previous_notification_state;
let mut komorebi_widgets = Vec::new();
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
@@ -422,18 +426,19 @@ impl Komobar {
komorebi_widgets
.into_iter()
.for_each(|(mut widget, idx, side)| {
match monitor_info {
match komorebi_notification_state {
None => {
monitor_info = Some(widget.monitor_info.clone());
komorebi_notification_state =
Some(widget.komorebi_notification_state.clone());
}
Some(ref previous) => {
if widget.workspaces.is_some() {
previous
.borrow_mut()
.update_from_self(&widget.monitor_info.borrow());
if widget.workspaces.is_some_and(|w| w.enable) {
previous.borrow_mut().update_from_config(
&widget.komorebi_notification_state.borrow(),
);
}
widget.monitor_info = previous.clone();
widget.komorebi_notification_state = previous.clone();
}
}
@@ -453,24 +458,23 @@ 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| {
let monitor = info.borrow();
let mapped_state = self.komorebi_notification_state.as_ref().map(|state| {
let state = state.borrow();
(
monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
monitor.mouse_follows_focus,
state.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
state.mouse_follows_focus,
)
});
if let Some(info) = mapped_info {
self.monitor_index = info.0;
self.mouse_follows_focus = info.1;
if let Some(state) = mapped_state {
self.monitor_index = state.0;
self.mouse_follows_focus = state.1;
}
if let Some(monitor_index) = self.monitor_index {
@@ -522,15 +526,11 @@ impl Komobar {
}
}
}
} else if self.monitor_info.is_some() && !self.disabled {
tracing::warn!(
"couldn't find the monitor index of this bar! Disabling the bar until the monitor connects..."
);
} else if self.komorebi_notification_state.is_some() && !self.disabled {
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
self.disabled = true;
} else {
tracing::warn!(
"couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi."
);
tracing::warn!("couldn't find the monitor index of this bar, if the bar is starting up this is normal until it receives the first state from komorebi.");
self.disabled = true;
}
@@ -566,7 +566,7 @@ impl Komobar {
tracing::info!("widget configuration options applied");
self.monitor_info = monitor_info;
self.komorebi_notification_state = komorebi_notification_state;
}
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
@@ -603,9 +603,7 @@ impl Komobar {
end.x -= margin.left + margin.right;
if end.y == 0.0 {
tracing::warn!(
"position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default"
)
tracing::warn!("position.end.y is set to 0.0 which will make your bar invisible on a config reload - this is usually set to 50.0 by default")
}
self.size_rect = komorebi_client::Rect {
@@ -637,7 +635,8 @@ impl Komobar {
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
home
@@ -651,6 +650,26 @@ impl Komobar {
match komorebi_client::StaticConfig::read(&config) {
Ok(config) => {
if let Some(theme) = config.theme {
let stack_accent = match theme {
KomorebiTheme::Catppuccin {
name, stack_border, ..
} => stack_border
.unwrap_or(CatppuccinValue::Green)
.color32(name.as_theme()),
KomorebiTheme::Base16 {
name, stack_border, ..
} => stack_border
.unwrap_or(Base16Value::Base0B)
.color32(Base16Wrapper::Base16(name)),
KomorebiTheme::Custom {
ref colours,
stack_border,
..
} => stack_border
.unwrap_or(Base16Value::Base0B)
.color32(Base16Wrapper::Custom(colours.clone())),
};
apply_theme(
ctx,
KomobarTheme::from(theme),
@@ -660,6 +679,10 @@ impl Komobar {
bar_grouping,
self.render_config.clone(),
);
if let Some(state) = &self.komorebi_notification_state {
state.borrow_mut().stack_accent = Some(stack_accent);
}
}
}
Err(_) => {
@@ -672,16 +695,17 @@ impl Komobar {
| Grouping::Alignment(config)
| Grouping::Widget(config),
) = &bar_grouping
&& let Some(rounding) = config.rounding
{
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.corner_radius =
rounding.into();
style.visuals.widgets.inactive.corner_radius = rounding.into();
style.visuals.widgets.hovered.corner_radius = rounding.into();
style.visuals.widgets.active.corner_radius = rounding.into();
style.visuals.widgets.open.corner_radius = rounding.into();
});
if let Some(rounding) = config.rounding {
ctx.style_mut(|style| {
style.visuals.widgets.noninteractive.corner_radius =
rounding.into();
style.visuals.widgets.inactive.corner_radius = rounding.into();
style.visuals.widgets.hovered.corner_radius = rounding.into();
style.visuals.widgets.active.corner_radius = rounding.into();
style.visuals.widgets.open.corner_radius = rounding.into();
});
}
}
}
}
@@ -701,7 +725,7 @@ impl Komobar {
disabled: false,
config,
render_config: Rc::new(RefCell::new(RenderConfig::new())),
monitor_info: None,
komorebi_notification_state: None,
left_widgets: vec![],
center_widgets: vec![],
right_widgets: vec![],
@@ -847,12 +871,12 @@ impl eframe::App for Komobar {
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
self.apply_config(ctx, self.monitor_info.clone());
self.apply_config(ctx, self.komorebi_notification_state.clone());
}
if let Ok(updated_config) = self.rx_config.try_recv() {
self.config = updated_config;
self.apply_config(ctx, self.monitor_info.clone());
self.apply_config(ctx, self.komorebi_notification_state.clone());
}
match self.rx_gui.try_recv() {
@@ -867,13 +891,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;
@@ -946,9 +966,9 @@ impl eframe::App for Komobar {
) {
let monitor_index = self.monitor_index.expect("should have a monitor index");
let monitor_size = state.monitors.elements()[monitor_index].size;
let monitor_size = state.monitors.elements()[monitor_index].size();
self.update_monitor_coordinates(&monitor_size);
self.update_monitor_coordinates(monitor_size);
should_apply_config = true;
}
@@ -959,7 +979,7 @@ impl eframe::App for Komobar {
// Check if monitor coordinates/size has changed
if let Some(monitor_index) = self.monitor_index {
let monitor_size = state.monitors.elements()[monitor_index].size;
let monitor_size = state.monitors.elements()[monitor_index].size();
let top = MONITOR_TOP.load(Ordering::SeqCst);
let left = MONITOR_LEFT.load(Ordering::SeqCst);
let right = MONITOR_RIGHT.load(Ordering::SeqCst);
@@ -969,38 +989,36 @@ impl eframe::App for Komobar {
bottom: monitor_size.bottom,
right,
};
if monitor_size != rect {
if *monitor_size != rect {
tracing::info!(
"Monitor coordinates/size has changed, storing new coordinates: {:#?}",
monitor_size
);
self.update_monitor_coordinates(&monitor_size);
self.update_monitor_coordinates(monitor_size);
should_apply_config = true;
}
}
if let Some(monitor_info) = &self.monitor_info {
monitor_info.borrow_mut().update(
self.monitor_index,
notification.state,
self.render_config.borrow().show_all_icons,
);
handle_notification(
ctx,
notification.event,
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
self.config.grouping,
self.config.theme.clone(),
self.render_config.clone(),
);
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
komorebi_notification_state
.borrow_mut()
.handle_notification(
ctx,
self.monitor_index,
notification,
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
self.config.grouping,
self.config.theme.clone(),
self.render_config.clone(),
);
}
if should_apply_config {
self.apply_config(ctx, self.monitor_info.clone());
self.apply_config(ctx, self.komorebi_notification_state.clone());
// Reposition the Bar
self.position_bar();
@@ -1326,66 +1344,3 @@ pub enum Alignment {
Center,
Right,
}
#[allow(clippy::too_many_arguments)]
fn handle_notification(
ctx: &Context,
event: komorebi_client::NotificationEvent,
bg_color: Rc<RefCell<Color32>>,
bg_color_with_alpha: Rc<RefCell<Color32>>,
transparency_alpha: Option<u8>,
grouping: Option<Grouping>,
default_theme: Option<KomobarTheme>,
render_config: Rc<RefCell<RenderConfig>>,
) {
if let NotificationEvent::Socket(message) = event {
match message {
SocketMessage::ReloadStaticConfiguration(path) => {
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(
ctx,
KomobarTheme::from(theme),
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from updated komorebi.json");
} else if let Some(default_theme) = default_theme {
apply_theme(
ctx,
default_theme,
bg_color.clone(),
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!(
"removed theme from updated komorebi.json and applied default theme"
);
} else {
tracing::warn!(
"theme was removed from updated komorebi.json but there was no default theme to apply"
);
}
}
}
SocketMessage::Theme(theme) => {
apply_theme(
ctx,
KomobarTheme::from(*theme),
bg_color,
bg_color_with_alpha.clone(),
transparency_alpha,
grouping,
render_config,
);
tracing::info!("applied theme from komorebi socket message");
}
_ => {}
}
}
}

View File

@@ -1,10 +1,11 @@
use crate::DEFAULT_PADDING;
use crate::bar::exec_powershell;
use crate::render::Grouping;
use crate::widgets::widget::WidgetConfig;
use crate::DEFAULT_PADDING;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
use eframe::egui::Vec2;
use komorebi_client::KomorebiTheme;
use komorebi_client::PathExt;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
@@ -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.40`
/// The `komorebi.bar.json` configuration file reference for `v0.1.38`
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>,
@@ -126,9 +120,7 @@ impl KomobarConfig {
}
if display {
println!(
"\nYour bar configuration file contains some options that have been renamed or deprecated:\n"
);
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
for (canonical, aliases) in map {
for alias in aliases {
if raw.contains(alias) {
@@ -151,7 +143,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 +154,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 +162,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 +171,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 +188,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 +235,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,
}
@@ -359,8 +332,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:
@@ -396,22 +367,21 @@ pub enum MouseMessage {
/// }
/// }
/// ```
#[serde(untagged)]
Komorebi(KomorebiMouseMessage),
/// Execute a custom command.
/// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved.
/// Example: `komorebic toggle-pause`
#[serde(untagged)]
Command(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi socket mouse message
pub struct KomorebiMouseMessage {
/// Send the FocusMonitorAtCursor message
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
/// 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 +389,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 +401,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>,
@@ -487,7 +454,7 @@ impl MouseMessage {
tracing::debug!("Sending messages: {messages:?}");
if komorebi_client::send_batch(messages).is_err() {
if komorebi_client::send_batch(messages.into_iter()).is_err() {
tracing::error!("could not send commands");
}
}
@@ -527,7 +494,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 +519,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 +597,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 +611,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 +634,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

View File

@@ -15,10 +15,10 @@ use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_client::replace_env_in_path;
use komorebi_client::PathExt;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use komorebi_client::replace_env_in_path;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
@@ -32,8 +32,8 @@ use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
use windows_core::BOOL;
@@ -103,7 +103,7 @@ fn process_hwnd() -> Option<isize> {
}
pub enum KomorebiEvent {
Notification(Box<komorebi_client::Notification>),
Notification(komorebi_client::Notification),
Reconnect,
}
@@ -114,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::gen::SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
println!("{schema}");
std::process::exit(0);
@@ -130,17 +137,13 @@ fn main() -> color_eyre::Result<()> {
}
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
unsafe {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
color_eyre::install()?;
if std::env::var("RUST_LOG").is_err() {
unsafe {
std::env::set_var("RUST_LOG", "info");
}
std::env::set_var("RUST_LOG", "info");
}
tracing::subscriber::set_global_default(
@@ -156,7 +159,8 @@ fn main() -> color_eyre::Result<()> {
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
home
@@ -216,30 +220,28 @@ 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)
.map_or(usr_monitor_index, |i| *i);
MONITOR_RIGHT.store(
state.monitors.elements()[monitor_index].size.right,
state.monitors.elements()[monitor_index].size().right,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[monitor_index].size.top,
state.monitors.elements()[monitor_index].size().top,
Ordering::SeqCst,
);
MONITOR_LEFT.store(
state.monitors.elements()[monitor_index].size.left,
state.monitors.elements()[monitor_index].size().left,
Ordering::SeqCst,
);
@@ -249,11 +251,11 @@ fn main() -> color_eyre::Result<()> {
None => {
config.position = Some(PositionConfig {
start: Some(Position {
x: state.monitors.elements()[monitor_index].size.left as f32,
y: state.monitors.elements()[monitor_index].size.top as f32,
x: state.monitors.elements()[monitor_index].size().left as f32,
y: state.monitors.elements()[monitor_index].size().top as f32,
}),
end: Some(Position {
x: state.monitors.elements()[monitor_index].size.right as f32,
x: state.monitors.elements()[monitor_index].size().right as f32,
y: 50.0,
}),
})
@@ -261,14 +263,14 @@ fn main() -> color_eyre::Result<()> {
Some(ref mut position) => {
if position.start.is_none() {
position.start = Some(Position {
x: state.monitors.elements()[monitor_index].size.left as f32,
y: state.monitors.elements()[monitor_index].size.top as f32,
x: state.monitors.elements()[monitor_index].size().left as f32,
y: state.monitors.elements()[monitor_index].size().top as f32,
});
}
if position.end.is_none() {
position.end = Some(Position {
x: state.monitors.elements()[monitor_index].size.right as f32,
x: state.monitors.elements()[monitor_index].size().right as f32,
y: 50.0,
})
}
@@ -353,7 +355,7 @@ fn main() -> color_eyre::Result<()> {
while komorebi_client::send_message(
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
)
.is_err()
.is_err()
{
std::thread::sleep(Duration::from_secs(1));
}
@@ -376,7 +378,7 @@ fn main() -> color_eyre::Result<()> {
Ok(notification) => {
tracing::debug!("received notification from komorebi");
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(Box::new(notification))) {
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
tracing::error!("could not send komorebi notification update to gui thread: {error}")
}
@@ -404,5 +406,5 @@ fn main() -> color_eyre::Result<()> {
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config)))
}),
)
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
}

View File

@@ -1,8 +1,8 @@
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::bar::Alignment;
use crate::config::KomobarConfig;
use crate::config::MonitorConfigOrIndex;
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
@@ -18,33 +18,27 @@ use komorebi_client::Rgb;
use serde::Deserialize;
use serde::Serialize;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "kind")]
/// Grouping
pub enum Grouping {
/// No grouping is applied
#[cfg_attr(feature = "schemars", schemars(title = "None"))]
None,
/// Widgets are grouped as a whole
#[cfg_attr(feature = "schemars", schemars(title = "Bar"))]
Bar(GroupingConfig),
/// Widgets are grouped by alignment
#[cfg_attr(feature = "schemars", schemars(title = "Alignment"))]
Alignment(GroupingConfig),
/// Widgets are grouped individually
#[cfg_attr(feature = "schemars", schemars(title = "Widget"))]
Widget(GroupingConfig),
}
#[derive(Clone)]
/// Render configuration
pub struct RenderConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub monitor_idx: usize,
@@ -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

@@ -2,6 +2,7 @@ use super::ImageIcon;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::vec2;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
@@ -16,7 +17,6 @@ use eframe::egui::Stroke;
use eframe::egui::StrokeKind;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::vec2;
use komorebi_client::PathExt;
use serde::Deserialize;
use serde::Serialize;
@@ -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

@@ -2,31 +2,29 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use starship_battery::units::ratio::percent;
use starship_battery::Manager;
use starship_battery::State;
use starship_battery::units::ratio::percent;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Battery widget configuration
pub struct BatteryConfig {
/// Enable the Battery widget
pub enable: bool,
/// Hide the widget if the battery is at full charge
pub hide_on_full_charge: Option<bool>,
/// Data refresh interval 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>,
@@ -89,41 +87,41 @@ impl Battery {
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
output = None;
if let Ok(mut batteries) = self.manager.batteries()
&& let Some(Ok(first)) = batteries.nth(0)
{
let percentage = first.state_of_charge().get::<percent>().round() as u8;
if let Ok(mut batteries) = self.manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>().round() as u8;
if percentage == 100 && self.hide_on_full_charge {
output = None
} else {
match first.state() {
State::Charging => self.state = BatteryState::Charging,
State::Discharging => {
self.state = match percentage {
p if p > 75 => BatteryState::Discharging,
p if p > 50 => BatteryState::High,
p if p > 25 => BatteryState::Medium,
p if p > 10 => BatteryState::Low,
_ => BatteryState::Warning,
if percentage == 100 && self.hide_on_full_charge {
output = None
} else {
match first.state() {
State::Charging => self.state = BatteryState::Charging,
State::Discharging => {
self.state = match percentage {
p if p > 75 => BatteryState::Discharging,
p if p > 50 => BatteryState::High,
p if p > 25 => BatteryState::Medium,
p if p > 10 => BatteryState::Low,
_ => BatteryState::Warning,
}
}
_ => {}
}
_ => {}
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
output = Some(BatteryOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage}%")
}
LabelPrefix::None | LabelPrefix::Icon => {
format!("{percentage}%")
}
},
selected,
})
}
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
output = Some(BatteryOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage}%")
}
LabelPrefix::None | LabelPrefix::Icon => {
format!("{percentage}%")
}
},
selected,
})
}
}
@@ -178,11 +176,13 @@ impl BarWidget for Battery {
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
&& let Err(error) = Command::new("cmd.exe")
{
if let Err(error) = Command::new("cmd.exe")
.args(["/C", "start", "ms-settings:batterysaver"])
.spawn()
{
eprintln!("{error}")
{
eprintln!("{}", error)
}
}
});
}

View File

@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
@@ -18,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>,
@@ -78,8 +76,8 @@ impl Cpu {
CpuOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {used}%"),
LabelPrefix::None | LabelPrefix::Icon => format!("{used}%"),
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
},
selected,
}
@@ -122,10 +120,12 @@ impl BarWidget for Cpu {
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
&& let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{error}")
if let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
}
});
}

View File

@@ -4,16 +4,15 @@ use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use chrono::Local;
use chrono_tz::Tz;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
@@ -62,7 +61,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 +103,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 +113,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),
}
@@ -171,7 +166,7 @@ impl Date {
.to_string()
.trim()
.to_string(),
Err(_) => format!("Invalid timezone: {timezone}"),
Err(_) => format!("Invalid timezone: {}", timezone),
},
None => Local::now()
.format(&self.format.fmt_string())
@@ -230,7 +225,7 @@ impl BarWidget for Date {
if SelectableFrame::new(false)
.show(ui, |ui| {
ui.add(
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false),
)
})

View File

@@ -1,17 +1,15 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widgets::widget::BarWidget;
use color_eyre::eyre;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use windows::Win32::Globalization::LCIDToLocaleName;
@@ -21,16 +19,15 @@ use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::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 +35,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,
@@ -81,7 +80,7 @@ pub struct Keyboard {
/// - `Ok(String)`: The name of the active keyboard layout as a valid UTF-8 string.
/// - `Err(())`: Indicates that the function failed to retrieve the locale name or encountered
/// invalid UTF-16 characters during conversion.
fn get_active_keyboard_layout() -> eyre::Result<String, ()> {
fn get_active_keyboard_layout() -> Result<String, ()> {
let foreground_window_tid = unsafe { GetWindowThreadProcessId(GetForegroundWindow(), None) };
let lcid = unsafe { GetKeyboardLayout(foreground_window_tid) };
@@ -170,10 +169,7 @@ impl BarWidget for Keyboard {
);
config.apply_on_widget(true, ui, |ui| {
ui.add(
Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone())))
.selectable(false),
)
ui.add(Label::new(WidgetText::LayoutJob(layout_job.clone())).selectable(false))
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ use crate::config::DisplayFormat;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::komorebi::KomorebiLayoutConfig;
use color_eyre::eyre;
use eframe::egui::vec2;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
use eframe::egui::FontId;
@@ -13,12 +13,11 @@ use eframe::egui::Stroke;
use eframe::egui::StrokeKind;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::vec2;
use komorebi_client::SocketMessage;
use serde::de::Error;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::de::Error;
use serde_json::from_str;
use std::fmt::Display;
use std::fmt::Formatter;
@@ -26,30 +25,24 @@ use std::fmt::Formatter;
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Komorebi layout kind
pub enum KomorebiLayout {
/// Predefined layout
#[cfg_attr(feature = "schemars", schemars(title = "Default"))]
Default(komorebi_client::DefaultLayout),
/// Monocle mode
Monocle,
/// Floating layer
Floating,
/// Paused
Paused,
/// Custom layout
Custom,
}
impl<'de> Deserialize<'de> for KomorebiLayout {
fn deserialize<D>(deserializer: D) -> eyre::Result<Self, D::Error>
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
// Attempt to deserialize the string as a DefaultLayout
if let Ok(default_layout) = from_str::<komorebi_client::DefaultLayout>(&format!("\"{s}\""))
if let Ok(default_layout) =
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
{
return Ok(KomorebiLayout::Default(default_layout));
}
@@ -60,7 +53,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
"Floating" => Ok(KomorebiLayout::Floating),
"Paused" => Ok(KomorebiLayout::Paused),
"Custom" => Ok(KomorebiLayout::Custom),
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
}
}
}
@@ -99,15 +92,16 @@ impl KomorebiLayout {
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
match self {
KomorebiLayout::Default(option) => {
if let Some(ws_idx) = workspace_idx
&& komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
if let Some(ws_idx) = workspace_idx {
if komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
monitor_idx,
ws_idx,
*option,
))
.is_err()
{
tracing::error!("could not send message to komorebi: WorkspaceLayout");
{
tracing::error!("could not send message to komorebi: WorkspaceLayout");
}
}
}
KomorebiLayout::Monocle => {
@@ -276,53 +270,57 @@ impl KomorebiLayout {
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
}
if show_options && let Some(workspace_idx) = workspace_idx {
Frame::NONE.show(ui, |ui| {
ui.add(
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
.selectable(false),
);
if show_options {
if let Some(workspace_idx) = workspace_idx {
Frame::NONE.show(ui, |ui| {
ui.add(
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
.selectable(false),
);
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::RightMainVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::HorizontalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::UltrawideVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
//KomorebiLayout::Custom,
KomorebiLayout::Monocle,
KomorebiLayout::Floating,
KomorebiLayout::Paused,
]);
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::RightMainVerticalStack,
),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::HorizontalStack,
),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::UltrawideVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
//KomorebiLayout::Custom,
KomorebiLayout::Monocle,
KomorebiLayout::Floating,
KomorebiLayout::Paused,
]);
for layout_option in &mut layout_options {
let is_selected = self == layout_option;
for layout_option in &mut layout_options {
let is_selected = self == layout_option;
if SelectableFrame::new(is_selected)
.show(ui, |ui| {
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
})
.on_hover_text(match layout_option {
KomorebiLayout::Default(layout) => layout.to_string(),
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
KomorebiLayout::Floating => "Toggle tiling".to_string(),
KomorebiLayout::Paused => "Toggle pause".to_string(),
KomorebiLayout::Custom => "Custom".to_string(),
})
.clicked()
{
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
show_options = false;
};
}
});
if SelectableFrame::new(is_selected)
.show(ui, |ui| {
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
})
.on_hover_text(match layout_option {
KomorebiLayout::Default(layout) => layout.to_string(),
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
KomorebiLayout::Floating => "Toggle tiling".to_string(),
KomorebiLayout::Paused => "Toggle pause".to_string(),
KomorebiLayout::Custom => "Custom".to_string(),
})
.clicked()
{
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
show_options = false;
};
}
});
}
}
});

View File

@@ -1,15 +1,15 @@
use crate::MAX_LABEL_WIDTH;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widgets::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
@@ -17,7 +17,6 @@ use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Media widget configuration
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,
@@ -41,34 +40,36 @@ impl Media {
enable,
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
.unwrap()
.join()
.get()
.unwrap(),
}
}
pub fn toggle(&self) {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(op) = session.TryTogglePlayPauseAsync()
{
op.join().unwrap_or_default();
if let Ok(session) = self.session_manager.GetCurrentSession() {
if let Ok(op) = session.TryTogglePlayPauseAsync() {
op.get().unwrap_or_default();
}
}
}
fn output(&mut self) -> String {
if let Ok(session) = self.session_manager.GetCurrentSession()
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
&& let Ok(properties) = operation.join()
&& let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title())
{
if artist.is_empty() {
return format!("{title}");
}
if let Ok(session) = self.session_manager.GetCurrentSession() {
if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
if let Ok(properties) = operation.get() {
if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
if artist.is_empty() {
return format!("{title}");
}
if title.is_empty() {
return format!("{artist}");
}
if title.is_empty() {
return format!("{artist}");
}
return format!("{artist} - {title}");
return format!("{artist} - {title}");
}
}
}
}
String::new()

View File

@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
@@ -18,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>,
@@ -81,9 +79,9 @@ impl Memory {
MemoryOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {usage}%")
format!("RAM: {}%", usage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{usage}%"),
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
},
selected,
}
@@ -126,10 +124,12 @@ impl BarWidget for Memory {
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
&& let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{error}")
if let Err(error) =
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
}
});
}

View File

@@ -3,30 +3,24 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use num_derive::FromPrimitive;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network widget configuration
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
@@ -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>,
@@ -66,35 +58,24 @@ 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)),
default_refresh_interval,
networks_network_activity: Networks::new_with_refreshed_list(),
default_interface: String::new(),
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()
.checked_sub(Duration::from_secs(default_refresh_interval))
last_state_total_activity: vec![],
last_state_activity: vec![],
last_updated_network_activity: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
last_state_total_activity: Arc::new(Mutex::new(vec![])),
last_state_activity: Arc::new(Mutex::new(vec![])),
last_update_request_network_activity: Arc::new(Mutex::new(
Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
)),
activity_generation: Arc::new(AtomicU64::new(0)),
}
}
}
@@ -104,139 +85,84 @@ pub struct Network {
pub show_total_activity: bool,
pub show_activity: bool,
pub show_default_interface: bool,
networks_network_activity: Arc<Mutex<Networks>>,
default_refresh_interval: u64,
networks_network_activity: Networks,
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_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) {
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
}
});
}
}
fn default_interface(&mut self) -> String {
let current = self.default_interface.lock().unwrap().clone();
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let mut activity = self.last_state_activity.clone();
let mut total_activity = self.last_state_total_activity.clone();
let now = Instant::now();
if now.duration_since(self.last_update_request_default_interface)
> Duration::from_secs(self.default_refresh_interval)
if now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
self.last_update_request_default_interface = now;
self.update_default_interface_async();
}
activity.clear();
total_activity.clear();
current
}
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
fn update_network_activity_async(&mut self) {
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);
self.networks_network_activity.refresh(true);
thread::spawn(move || {
let mut activity = Vec::new();
let mut total_activity = Vec::new();
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
if self.show_activity {
let received = Self::to_pretty_bytes(
data.received(),
self.data_refresh_interval,
);
let transmitted = Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
);
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);
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
ReadingValue::from(received),
ReadingValue::from(transmitted),
));
}
for (interface_name, data) in &*networks {
if friendly_name.eq(interface_name) {
if show_activity {
let received =
Network::to_pretty_bytes(data.received(), data_refresh_interval);
let transmitted =
Network::to_pretty_bytes(data.transmitted(), data_refresh_interval);
if self.show_total_activity {
let total_received =
Self::to_pretty_bytes(data.total_received(), 1);
let total_transmitted =
Self::to_pretty_bytes(data.total_transmitted(), 1);
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
ReadingValue::from(received),
ReadingValue::from(transmitted),
));
}
if show_total_activity {
let total_received = Network::to_pretty_bytes(data.total_received(), 1);
let total_transmitted =
Network::to_pretty_bytes(data.total_transmitted(), 1);
total_activity.push(NetworkReading::new(
NetworkReadingFormat::Total,
ReadingValue::from(total_received),
ReadingValue::from(total_transmitted),
));
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)
}
@@ -386,9 +312,10 @@ impl Network {
if SelectableFrame::new_auto(selected, auto_focus_fill)
.show(ui, add_contents)
.clicked()
&& let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
{
eprintln!("{error}");
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error);
}
}
}
}
@@ -507,9 +434,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 +450,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(),
@@ -608,6 +535,6 @@ enum DataUnit {
impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{self:?}")
write!(f, "{:?}", self)
}
}

View File

@@ -3,12 +3,12 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
@@ -18,21 +18,13 @@ 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)))]
pub show_read_only_disks: Option<bool>,
/// Show removable disks
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
pub show_removable_disks: Option<bool>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
/// Hide when the current percentage is under this value [[1-100]]
@@ -46,8 +38,6 @@ impl From<StorageConfig> for Storage {
disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
show_removable_disks: value.show_removable_disks.unwrap_or(true),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
last_updated: Instant::now(),
@@ -65,8 +55,6 @@ pub struct Storage {
disks: Disks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
show_read_only_disks: bool,
show_removable_disks: bool,
auto_select_over: Option<u8>,
auto_hide_under: Option<u8>,
last_updated: Instant,
@@ -83,12 +71,6 @@ impl Storage {
let mut disks = vec![];
for disk in &self.disks {
if disk.is_read_only() && !self.show_read_only_disks {
continue;
}
if disk.is_removable() && !self.show_removable_disks {
continue;
}
let mount = disk.mount_point();
let total = disk.total_space();
let available = disk.available_space();
@@ -105,7 +87,7 @@ impl Storage {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), percentage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
},
selected,
})
@@ -160,15 +142,17 @@ impl BarWidget for Storage {
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
&& let Err(error) = Command::new("cmd.exe")
{
if let Err(error) = Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
output.label.split(' ').collect::<Vec<&str>>()[0],
])
.spawn()
{
eprintln!("{error}")
{
eprintln!("{}", error)
}
}
});
}

View File

@@ -6,6 +6,7 @@ use crate::widgets::widget::BarWidget;
use chrono::Local;
use chrono::NaiveTime;
use chrono_tz::Tz;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
@@ -15,7 +16,6 @@ use eframe::egui::Stroke;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::text::LayoutJob;
use eframe::epaint::StrokeKind;
use lazy_static::lazy_static;
use serde::Deserialize;
@@ -72,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),
}
@@ -212,7 +209,7 @@ impl Time {
Some(dt.time()),
)
}
Err(_) => (format!("Invalid timezone: {timezone:?}"), None),
Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
},
None => {
let dt = Local::now();

View File

@@ -2,12 +2,12 @@ use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
@@ -16,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>,
@@ -142,14 +140,16 @@ impl BarWidget for Update {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
&& let Err(error) = Command::new("explorer.exe")
{
if let Err(error) = Command::new("explorer.exe")
.args([format!(
"https://github.com/LGUG2Z/komorebi/releases/v{}",
self.latest_version
)])
.spawn()
{
eprintln!("{error}")
{
eprintln!("{}", error)
}
}
});
}

View File

@@ -34,43 +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),
/// Time widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
Time(TimeConfig),
/// Update widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Update"))]
Update(UpdateConfig),
}

View File

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

View File

@@ -1,36 +1,8 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::AnimationsConfig;
pub use komorebi::AppSpecificConfigurationPath;
pub use komorebi::AspectRatio;
pub use komorebi::BorderColours;
pub use komorebi::Colour;
pub use komorebi::CrossBoundaryBehaviour;
pub use komorebi::GridLayoutOptions;
pub use komorebi::KomorebiTheme;
pub use komorebi::LayoutOptions;
pub use komorebi::MonitorConfig;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::Placement;
pub use komorebi::PredefinedAspectRatio;
pub use komorebi::Rgb;
pub use komorebi::RuleDebug;
pub use komorebi::ScrollingLayoutOptions;
pub use komorebi::StackbarConfig;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
pub use komorebi::ThemeOptions;
pub use komorebi::VirtualDesktopNotification;
pub use komorebi::Wallpaper;
pub use komorebi::WindowContainerBehaviour;
pub use komorebi::WindowHandlingBehaviour;
pub use komorebi::WindowsApi;
pub use komorebi::WorkspaceConfig;
pub use komorebi::animation::PerAnimationPrefixConfig;
pub use komorebi::animation::prefix::AnimationPrefix;
pub use komorebi::animation::PerAnimationPrefixConfig;
pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::border_manager::BorderInfo;
pub use komorebi::config_generation::ApplicationConfiguration;
@@ -39,6 +11,8 @@ pub use komorebi::config_generation::IdWithIdentifierAndComment;
pub use komorebi::config_generation::MatchingRule;
pub use komorebi::config_generation::MatchingStrategy;
pub use komorebi::container::Container;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::replace_env_in_path;
pub use komorebi::core::AnimationStyle;
pub use komorebi::core::ApplicationIdentifier;
pub use komorebi::core::Arrangement;
@@ -68,24 +42,41 @@ pub use komorebi::core::StackbarLabel;
pub use komorebi::core::StackbarMode;
pub use komorebi::core::StateQuery;
pub use komorebi::core::WindowKind;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::replace_env_in_path;
pub use komorebi::monitor::Monitor;
pub use komorebi::monitor_reconciliator::MonitorNotification;
pub use komorebi::ring::Ring;
pub use komorebi::splash;
pub use komorebi::state::GlobalState;
pub use komorebi::state::State;
pub use komorebi::win32_display_data;
pub use komorebi::window::Window;
pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace;
pub use komorebi::workspace::WorkspaceGlobals;
pub use komorebi::workspace::WorkspaceLayer;
pub use komorebi::AnimationsConfig;
pub use komorebi::AppSpecificConfigurationPath;
pub use komorebi::AspectRatio;
pub use komorebi::BorderColours;
pub use komorebi::Colour;
pub use komorebi::CrossBoundaryBehaviour;
pub use komorebi::GlobalState;
pub use komorebi::KomorebiTheme;
pub use komorebi::MonitorConfig;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::PredefinedAspectRatio;
pub use komorebi::Rgb;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
pub use komorebi::VirtualDesktopNotification;
pub use komorebi::WindowContainerBehaviour;
pub use komorebi::WindowsApi;
pub use komorebi::WorkspaceConfig;
use komorebi::DATA_DIR;
use std::borrow::Borrow;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
@@ -103,15 +94,12 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
stream.write_all(serde_json::to_string(message)?.as_bytes())
}
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()>
where
Q: Borrow<SocketMessage>,
{
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
if let Ok(m_str) = serde_json::to_string(m.borrow()) {
if let Ok(m_str) = serde_json::to_string(&m) {
s.push_str(&m_str);
s.push('\n');
}

View File

@@ -1,7 +1,7 @@
[package]
name = "komorebi-gui"
version = "0.1.40"
edition = "2024"
version = "0.1.38"
edition = "2021"
# 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

@@ -1,9 +1,9 @@
#![warn(clippy::all)]
use eframe::egui;
use eframe::egui::color_picker::Alpha;
use eframe::egui::Color32;
use eframe::egui::ViewportBuilder;
use eframe::egui::color_picker::Alpha;
use komorebi_client::BorderStyle;
use komorebi_client::Colour;
use komorebi_client::DefaultLayout;
@@ -78,8 +78,8 @@ impl From<&komorebi_client::Monitor> for MonitorConfig {
}
Self {
size: value.size,
work_area_offset: value.work_area_offset.unwrap_or_default(),
size: *value.size(),
work_area_offset: value.work_area_offset().unwrap_or_default(),
workspaces,
}
}
@@ -95,22 +95,22 @@ struct WorkspaceConfig {
impl From<&komorebi_client::Workspace> for WorkspaceConfig {
fn from(value: &komorebi_client::Workspace) -> Self {
let layout = match value.layout {
Layout::Default(layout) => layout,
let layout = match value.layout() {
Layout::Default(layout) => *layout,
Layout::Custom(_) => DefaultLayout::BSP,
};
let name = value
.name
.name()
.to_owned()
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
Self {
layout,
name,
tile: value.tile,
workspace_padding: value.workspace_padding.unwrap_or(20),
container_padding: value.container_padding.unwrap_or(20),
tile: *value.tile(),
workspace_padding: value.workspace_padding().unwrap_or(20),
container_padding: value.container_padding().unwrap_or(20),
}
}
}
@@ -247,7 +247,7 @@ impl eframe::App for KomorebiGui {
egui::CentralPanel::default().show(ctx, |ui| {
ctx.set_pixels_per_point(2.0);
egui::ScrollArea::vertical().show(ui, |ui| {
ui.set_width(ctx.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);
@@ -437,7 +437,7 @@ impl eframe::App for KomorebiGui {
BorderStyle::Square,
] {
if ui
.add(egui::Button::selectable(
.add(egui::SelectableLabel::new(
self.border_config.border_style == option,
option.to_string(),
))
@@ -494,7 +494,7 @@ impl eframe::App for KomorebiGui {
StackbarMode::Always,
] {
if ui
.add(egui::Button::selectable(
.add(egui::SelectableLabel::new(
self.stackbar_config.mode == option,
option.to_string(),
))
@@ -513,7 +513,7 @@ impl eframe::App for KomorebiGui {
ui.collapsing("Label", |ui| {
for option in [StackbarLabel::Process, StackbarLabel::Title] {
if ui
.add(egui::Button::selectable(
.add(egui::SelectableLabel::new(
self.stackbar_config.label == option,
option.to_string(),
))
@@ -772,7 +772,7 @@ impl eframe::App for KomorebiGui {
DefaultLayout::Grid,
] {
if ui
.add(egui::Button::selectable(
.add(egui::SelectableLabel::new(
workspace.layout == option,
option.to_string(),
))

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,5 +1,6 @@
use eframe::egui::ViewportBuilder;
use std::path::PathBuf;
use whkd_core::HotkeyBinding;
use whkd_core::Whkdrc;
#[derive(Default)]
@@ -57,18 +58,21 @@ impl eframe::App for Quicklook {
ui.label("Filter");
ui.add(
eframe::egui::text_edit::TextEdit::singleline(&mut self.filter)
.hint_text("Filter by command...")
.background_color(ctx.style().visuals.faint_bg_color),
);
ui.end_row();
ui.end_row();
for binding in &whkdrc.bindings {
let keys = binding.keys.join(" + ");
if self.filter.is_empty() || binding.command.contains(&self.filter)
{
ui.label(keys);
ui.label(&binding.command);
ui.end_row();
if is_komorebic_binding(binding) {
let keys = binding.keys.join(" + ");
if self.filter.is_empty()
|| binding.command.contains(&self.filter)
{
ui.label(keys);
ui.label(&binding.command);
ui.end_row();
}
}
}
}
@@ -96,3 +100,7 @@ fn main() {
)
.unwrap();
}
fn is_komorebic_binding(binding: &HotkeyBinding) -> bool {
binding.command.starts_with("komorebic")
}

View File

@@ -1,14 +1,12 @@
[package]
name = "komorebi-themes"
version = "0.1.40"
edition = "2024"
version = "0.1.38"
edition = "2021"
[dependencies]
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "b9e26b31f7a0e7ed239b14e5317e95d1bdc544bd" }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a", default-features = false, features = [
"egui33",
] }
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "96f26c88d83781f234d42222293ec73d23a39ad8" }
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "bdaff30959512c4f7ee7304117076a48633d777f", default-features = false, features = ["egui31"] }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
eframe = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }

View File

@@ -1,8 +1,12 @@
use hex_color::HexColor;
#[cfg(feature = "schemars")]
use schemars::Schema;
use schemars::gen::SchemaGenerator;
#[cfg(feature = "schemars")]
use schemars::SchemaGenerator;
use schemars::schema::InstanceType;
#[cfg(feature = "schemars")]
use schemars::schema::Schema;
#[cfg(feature = "schemars")]
use schemars::schema::SchemaObject;
use crate::Color32;
use serde::Deserialize;
@@ -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

@@ -1,6 +1,6 @@
use crate::Base16ColourPalette;
use crate::colour::Colour;
use crate::colour::Hex;
use crate::Base16ColourPalette;
use hex_color::HexColor;
use std::collections::VecDeque;
use std::fmt::Display;
@@ -12,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

@@ -4,8 +4,8 @@
pub mod colour;
mod generator;
pub use generator::ThemeVariant;
pub use generator::generate_base16_palette;
pub use generator::ThemeVariant;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -16,31 +16,30 @@ use strum::IntoEnumIterator;
use crate::colour::Colour;
pub use base16_egui_themes::Base16;
pub use catppuccin_egui;
use eframe::egui::style::Selection;
use eframe::egui::style::WidgetVisuals;
use eframe::egui::style::Widgets;
pub use eframe::egui::Color32;
use eframe::egui::Shadow;
use eframe::egui::Stroke;
use eframe::egui::Style;
use eframe::egui::Visuals;
use eframe::egui::style::Selection;
use eframe::egui::style::WidgetVisuals;
use eframe::egui::style::Widgets;
use serde_variant::to_variant_name;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "type")]
/// Theme
pub enum Theme {
/// 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,25 +1,23 @@
[package]
name = "komorebi"
version = "0.1.40"
version = "0.1.38"
description = "A tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2024"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-themes = { path = "../komorebi-themes" }
base64 = "0.22"
bitflags = { version = "2", features = ["serde"] }
clap = { workspace = true }
chrono = { workspace = true }
color-eyre = { workspace = true }
crossbeam-channel = { workspace = true }
crossbeam-utils = { workspace = true }
ctrlc = { version = "3", features = ["termination"] }
dirs = { workspace = true }
ed25519-dalek = "2"
getset = "0.1"
hotwatch = { workspace = true }
lazy_static = { workspace = true }
miow = "0.6"
@@ -30,10 +28,9 @@ parking_lot = { workspace = true }
paste = { workspace = true }
powershell_script = "1.0"
regex = "1"
reqwest = { version = "0.12", features = ["blocking"] }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true, features = ["preserve_order"] }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
shadow-rs = { workspace = true }
strum = { workspace = true }
@@ -51,7 +48,7 @@ windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.55"
serde_with = { version = "3.12", features = ["schemars_1"] }
serde_with = { version = "3.12", features = ["schemars_0_8"] }
[build-dependencies]
shadow-rs = { workspace = true }

View File

@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use super::prefix::AnimationPrefix;

View File

@@ -1,4 +1,4 @@
use color_eyre::eyre;
use color_eyre::Result;
use serde::Deserialize;
use serde::Serialize;
@@ -6,10 +6,10 @@ use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use super::RenderDispatcher;
use super::ANIMATION_DURATION_GLOBAL;
use super::ANIMATION_FPS;
use super::ANIMATION_MANAGER;
use super::RenderDispatcher;
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@@ -55,9 +55,9 @@ impl AnimationEngine {
#[allow(clippy::cast_precision_loss)]
pub fn animate(
render_dispatcher: impl RenderDispatcher + Send + 'static,
render_dispatcher: (impl RenderDispatcher + Send + 'static),
duration: Duration,
) -> eyre::Result<()> {
) -> Result<()> {
std::thread::spawn(move || {
let animation_key = render_dispatcher.get_animation_key();
if ANIMATION_MANAGER.lock().in_progress(animation_key.as_str()) {

View File

@@ -1,5 +1,5 @@
use crate::AnimationStyle;
use crate::core::Rect;
use crate::AnimationStyle;
use super::style::apply_ease_func;

View File

@@ -4,9 +4,9 @@ use crate::core::animation::AnimationStyle;
use lazy_static::lazy_static;
use prefix::AnimationPrefix;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use parking_lot::Mutex;
@@ -25,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),
}

View File

@@ -17,5 +17,5 @@ pub enum AnimationPrefix {
}
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
format!("{prefix}:{key}")
format!("{}:{}", prefix, key)
}

View File

@@ -1,8 +1,8 @@
use color_eyre::eyre;
use color_eyre::Result;
pub trait RenderDispatcher {
fn get_animation_key(&self) -> String;
fn pre_render(&self) -> eyre::Result<()>;
fn render(&self, delta: f64) -> eyre::Result<()>;
fn post_render(&self) -> eyre::Result<()>;
fn pre_render(&self) -> Result<()>;
fn render(&self, delta: f64) -> Result<()>;
fn post_render(&self) -> Result<()>;
}

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

@@ -1,30 +1,33 @@
use crate::WINDOWS_11;
use crate::WindowsApi;
use crate::border_manager::window_kind_colour;
use crate::border_manager::RenderTarget;
use crate::border_manager::WindowKind;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::RenderTarget;
use crate::border_manager::STYLE;
use crate::border_manager::WindowKind;
use crate::border_manager::window_kind_colour;
use crate::core::BorderStyle;
use crate::core::Rect;
use crate::windows_api;
use crate::WindowsApi;
use crate::WINDOWS_11;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::LazyLock;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::sync::LazyLock;
use windows::Win32::Foundation::FALSE;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::TRUE;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
use windows::Win32::Graphics::Direct2D::Common::D2D1_ALPHA_MODE_PREMULTIPLIED;
use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F;
use windows::Win32::Graphics::Direct2D::Common::D2D1_PIXEL_FORMAT;
use windows::Win32::Graphics::Direct2D::Common::D2D_RECT_F;
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
use windows::Win32::Graphics::Direct2D::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE;
use windows::Win32::Graphics::Direct2D::D2D1_BRUSH_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_FACTORY_TYPE_MULTI_THREADED;
@@ -33,34 +36,31 @@ use windows::Win32::Graphics::Direct2D::D2D1_PRESENT_OPTIONS_IMMEDIATELY;
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_PROPERTIES;
use windows::Win32::Graphics::Direct2D::D2D1_RENDER_TARGET_TYPE_DEFAULT;
use windows::Win32::Graphics::Direct2D::D2D1_ROUNDED_RECT;
use windows::Win32::Graphics::Direct2D::D2D1CreateFactory;
use windows::Win32::Graphics::Direct2D::ID2D1Factory;
use windows::Win32::Graphics::Direct2D::ID2D1SolidColorBrush;
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
use windows::Win32::Graphics::Dwm::DWM_BB_BLURREGION;
use windows::Win32::Graphics::Dwm::DWM_BB_ENABLE;
use windows::Win32::Graphics::Dwm::DWM_BLURBEHIND;
use windows::Win32::Graphics::Dwm::DwmEnableBlurBehindWindow;
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_UNKNOWN;
use windows::Win32::Graphics::Gdi::CreateRectRgn;
use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::Graphics::Gdi::ValidateRect;
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
@@ -102,10 +102,10 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
let hwnds = unsafe { &mut *(lparam.0 as *mut Vec<isize>) };
let hwnd = hwnd.0 as isize;
if let Ok(class) = WindowsApi::real_window_class_w(hwnd)
&& class.starts_with("komoborder")
{
hwnds.push(hwnd);
if let Ok(class) = WindowsApi::real_window_class_w(hwnd) {
if class.starts_with("komoborder") {
hwnds.push(hwnd);
}
}
true.into()
@@ -313,11 +313,6 @@ impl Border {
}
pub fn destroy(&self) -> color_eyre::Result<()> {
// clear user data **BEFORE** closing window
// pending messages will see a null pointer and exit early
unsafe {
SetWindowLongPtrW(self.hwnd(), GWLP_USERDATA, 0);
}
WindowsApi::close_window(self.hwnd)
}
@@ -397,63 +392,63 @@ impl Border {
tracing::error!("failed to update border position {error}");
}
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;
if !rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect) {
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;
(*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,
(*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,
};
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.Resize(&D2D_SIZE_U {
width: rect.right as u32,
height: rect.bottom as u32,
});
let _ = render_target.EndDraw(None, None);
let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
render_target.BeginDraw();
render_target.Clear(None);
// Calculate border radius based on style
let style = match (*border_pointer).style {
BorderStyle::System => {
if *WINDOWS_11 {
BorderStyle::Rounded
} else {
BorderStyle::Square
}
}
BorderStyle::Rounded => BorderStyle::Rounded,
BorderStyle::Square => BorderStyle::Square,
};
match style {
BorderStyle::Rounded => {
render_target.DrawRoundedRectangle(
&(*border_pointer).rounded_rect,
brush,
border_width as f32,
None,
);
}
BorderStyle::Square => {
render_target.DrawRectangle(
&(*border_pointer).rounded_rect.rect,
brush,
border_width as f32,
None,
);
}
_ => {}
}
let _ = render_target.EndDraw(None, None);
}
}
}
@@ -548,12 +543,7 @@ impl Border {
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

@@ -1,8 +1,6 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
mod border;
use crate::WindowManager;
use crate::WindowsApi;
use crate::core::BorderImplementation;
use crate::core::BorderStyle;
use crate::core::WindowKind;
@@ -10,8 +8,10 @@ use crate::ring::Ring;
use crate::windows_api;
use crate::workspace::Workspace;
use crate::workspace::WorkspaceLayer;
pub use border::Border;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds;
pub use border::Border;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
@@ -22,15 +22,15 @@ use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use strum::Display;
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
@@ -170,15 +170,13 @@ fn window_kind_colour(focus_kind: WindowKind) -> u32 {
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || {
loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
@@ -211,9 +209,9 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
.iter()
.map(|w| w.hwnd)
.collect::<Vec<_>>();
let workspace_layer = state.monitors.elements()[focused_monitor_idx].workspaces()
let workspace_layer = *state.monitors.elements()[focused_monitor_idx].workspaces()
[focused_workspace_idx]
.layer;
.layer();
let foreground_window = WindowsApi::foreground_window().unwrap_or_default();
let layer_changed = previous_layer != workspace_layer;
let forced_update = matches!(notification, Notification::ForceUpdate);
@@ -226,7 +224,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() {
// Handle the monocle container separately
if let Some(monocle) = &ws.monocle_container {
if let Some(monocle) = ws.monocle_container() {
let window_kind = if monitor_idx != focused_monitor_idx {
WindowKind::Unfocused
} else {
@@ -239,7 +237,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
.unwrap_or_default()
.set_accent(window_kind_colour(window_kind))?;
if ws.layer == WorkspaceLayer::Floating {
if ws.layer() == &WorkspaceLayer::Floating {
for window in ws.floating_windows() {
let mut window_kind = WindowKind::Unfocused;
@@ -257,7 +255,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let window_kind = if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
{
if c.locked {
if ws.locked_containers().contains(&idx) {
WindowKind::UnfocusedLocked
} else {
WindowKind::Unfocused
@@ -341,11 +339,15 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
should_process_notification = true;
}
if !should_process_notification
&& let Some(Notification::Update(ref previous)) = previous_notification
&& previous.unwrap_or_default() != notification_hwnd.unwrap_or_default()
{
should_process_notification = true;
if !should_process_notification {
if let Some(Notification::Update(ref previous)) = previous_notification
{
if previous.unwrap_or_default()
!= notification_hwnd.unwrap_or_default()
{
should_process_notification = true;
}
}
}
should_process_notification
@@ -381,7 +383,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Only operate on the focused workspace of each monitor
if let Some(ws) = m.focused_workspace() {
// Workspaces with tiling disabled don't have borders
if !ws.tile {
if !ws.tile() {
// Remove all borders on this monitor
remove_borders(
&mut borders,
@@ -394,16 +396,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
// Handle the monocle container separately
if let Some(monocle) = &ws.monocle_container {
if let Some(monocle) = ws.monocle_container() {
let mut new_border = false;
let focused_window_hwnd =
monocle.focused_window().map(|w| w.hwnd).unwrap_or_default();
let id = monocle.id.clone();
let id = monocle.id().clone();
let border = match borders.entry(id.clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(
&monocle.id,
monocle.id(),
focused_window_hwnd,
monitor_idx,
) {
@@ -461,7 +463,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let border_hwnd = border.hwnd;
if ws.layer == WorkspaceLayer::Floating {
if ws.layer() == &WorkspaceLayer::Floating {
handle_floating_borders(
&mut borders,
&mut windows_borders,
@@ -500,8 +502,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let foreground_hwnd = WindowsApi::foreground_window().unwrap_or_default();
let foreground_monitor_id =
WindowsApi::monitor_from_window(foreground_hwnd);
let is_maximized =
foreground_monitor_id == m.id && WindowsApi::is_zoomed(foreground_hwnd);
let is_maximized = foreground_monitor_id == m.id()
&& WindowsApi::is_zoomed(foreground_hwnd);
if is_maximized {
// Remove all borders on this monitor
@@ -519,7 +521,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let mut container_and_floating_window_ids = ws
.containers()
.iter()
.map(|c| c.id.clone())
.map(|c| c.id().clone())
.collect::<Vec<_>>();
for w in ws.floating_windows() {
@@ -537,7 +539,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
'containers: for (idx, c) in ws.containers().iter().enumerate() {
let focused_window_hwnd =
c.focused_window().map(|w| w.hwnd).unwrap_or_default();
let id = c.id.clone();
let id = c.id().clone();
// Get the border entry for this container from the map or create one
let mut new_border = false;
@@ -545,7 +547,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) =
Border::create(&c.id, focused_window_hwnd, monitor_idx)
Border::create(c.id(), focused_window_hwnd, monitor_idx)
{
new_border = true;
entry.insert(border)
@@ -561,7 +563,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|| monitor_idx != focused_monitor_idx
|| focused_window_hwnd != foreground_window
{
if c.locked {
if ws.locked_containers().contains(&idx) {
WindowKind::UnfocusedLocked
} else {
WindowKind::Unfocused
@@ -601,7 +603,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let rect = match WindowsApi::window_rect(focused_window_hwnd) {
Ok(rect) => rect,
Err(_) => {
remove_border(&c.id, &mut borders, &mut windows_borders)?;
remove_border(c.id(), &mut borders, &mut windows_borders)?;
continue 'containers;
}
};
@@ -767,7 +769,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(())
@@ -814,26 +815,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

@@ -6,17 +6,17 @@
use std::ffi::c_void;
use std::ops::Deref;
use windows::core::IUnknown;
use windows::core::IUnknown_Vtbl;
use windows::core::GUID;
use windows::core::HRESULT;
use windows::core::HSTRING;
use windows::core::PCWSTR;
use windows::core::PWSTR;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::RECT;
use windows::Win32::Foundation::SIZE;
use windows::Win32::UI::Shell::Common::IObjectArray;
use windows::core::GUID;
use windows::core::HRESULT;
use windows::core::HSTRING;
use windows::core::IUnknown;
use windows::core::IUnknown_Vtbl;
use windows::core::PCWSTR;
use windows::core::PWSTR;
use windows_core::BOOL;
type DesktopID = GUID;
@@ -129,7 +129,7 @@ pub unsafe trait IApplicationView: IUnknown {
pub unsafe fn get_app_user_model_id(&self, id: *mut PWSTR) -> HRESULT; // Proc17
pub unsafe fn set_app_user_model_id(&self, id: PCWSTR) -> HRESULT;
pub unsafe fn is_equal_by_app_user_model_id(&self, id: PCWSTR, out_result: *mut INT)
-> HRESULT;
-> HRESULT;
/*** IApplicationView methods ***/
pub unsafe fn get_view_state(&self, out_state: *mut UINT) -> HRESULT; // Proc20

View File

@@ -11,11 +11,11 @@ use interfaces::IServiceProvider;
use std::ffi::c_void;
use windows::Win32::Foundation::HWND;
use windows::Win32::System::Com::CLSCTX_ALL;
use windows::Win32::System::Com::COINIT_MULTITHREADED;
use windows::Win32::System::Com::CoCreateInstance;
use windows::Win32::System::Com::CoInitializeEx;
use windows::Win32::System::Com::CoUninitialize;
use windows::Win32::System::Com::CLSCTX_ALL;
use windows::Win32::System::Com::COINIT_MULTITHREADED;
use windows_core::Interface;
struct ComInit();
@@ -64,7 +64,7 @@ fn get_iapplication_view_collection(provider: &IServiceProvider) -> IApplication
})
}
#[unsafe(no_mangle)]
#[no_mangle]
pub extern "C" fn SetCloak(hwnd: HWND, cloak_type: u32, flags: i32) {
COM_INIT.with(|_| {
let provider = get_iservice_provider();

View File

@@ -1,19 +1,18 @@
use std::collections::VecDeque;
use getset::Getters;
use nanoid::nanoid;
use serde::Deserialize;
use serde::Serialize;
use crate::Lockable;
use crate::ring::Ring;
use crate::window::Window;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Container {
pub id: String,
#[serde(default)]
pub locked: bool,
#[getset(get = "pub")]
id: String,
windows: Ring<Window>,
}
@@ -23,45 +22,22 @@ impl Default for Container {
fn default() -> Self {
Self {
id: nanoid!(),
locked: false,
windows: Ring::default(),
}
}
}
impl Lockable for Container {
fn locked(&self) -> bool {
self.locked
}
fn set_locked(&mut self, locked: bool) -> &mut Self {
self.locked = locked;
self
}
}
impl Container {
pub fn preselect() -> Self {
Self {
id: "PRESELECT".to_string(),
locked: false,
windows: Default::default(),
}
}
pub fn is_preselect(&self) -> bool {
self.id == "PRESELECT"
}
pub fn hide(&self, omit: Option<isize>) {
for window in self.windows().iter().rev() {
let mut should_hide = omit.is_none();
if !should_hide
&& let Some(omit) = omit
&& omit != window.hwnd
{
should_hide = true
if !should_hide {
if let Some(omit) = omit {
if omit != window.hwnd {
should_hide = true
}
}
}
if should_hide {
@@ -93,10 +69,10 @@ impl Container {
pub fn hwnd_from_exe(&self, exe: &str) -> Option<isize> {
for window in self.windows() {
if let Ok(window_exe) = window.exe()
&& exe == window_exe
{
return Option::from(window.hwnd);
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(window.hwnd);
}
}
}
@@ -105,10 +81,10 @@ impl Container {
pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
for (idx, window) in self.windows().iter().enumerate() {
if let Ok(window_exe) = window.exe()
&& exe == window_exe
{
return Option::from(idx);
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(idx);
}
}
}
@@ -168,7 +144,6 @@ impl Container {
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
#[test]
fn test_contains_window() {
@@ -275,40 +250,4 @@ mod tests {
// Should return None since window 4 doesn't exist
assert_eq!(container.idx_for_window(4), None);
}
#[test]
fn deserializes_with_missing_locked_field_defaults_to_false() {
let json = r#"{
"id": "test-1",
"windows": { "elements": [], "focused": 0 }
}"#;
let container: Container = serde_json::from_str(json).expect("Should deserialize");
assert!(!container.locked);
assert_eq!(container.id, "test-1");
assert!(container.windows().is_empty());
let json = r#"{
"id": "test-2",
"windows": { "elements": [ { "hwnd": 5 }, { "hwnd": 9 } ], "focused": 1 }
}"#;
let container: Container = serde_json::from_str(json).unwrap();
assert_eq!(container.id, "test-2");
assert!(!container.locked);
assert_eq!(container.windows(), &[Window::from(5), Window::from(9)]);
assert_eq!(container.focused_window_idx(), 1);
}
#[test]
fn serializes_and_deserializes() {
let mut container = Container::default();
container.set_locked(true);
let serialized = serde_json::to_string(&container).expect("Should serialize");
let deserialized: Container =
serde_json::from_str(&serialized).expect("Should deserialize");
assert!(deserialized.locked);
assert_eq!(deserialized.id, container.id);
}
}

View File

@@ -1,80 +1,45 @@
use clap::ValueEnum;
use serde::ser::SerializeSeq;
use serde::Deserialize;
use serde::Serialize;
use serde::ser::SerializeSeq;
use strum::Display;
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 Bézier function
CubicBezier(f64, f64, f64, f64),
}

View File

@@ -6,12 +6,12 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use super::CustomLayout;
use super::DefaultLayout;
use super::Rect;
use super::custom_layout::Column;
use super::custom_layout::ColumnSplit;
use super::custom_layout::ColumnSplitWithCapacity;
use super::CustomLayout;
use super::DefaultLayout;
use super::Rect;
use crate::default_layout::LayoutOptions;
pub trait Arrangement {
@@ -49,86 +49,97 @@ impl Arrangement for DefaultLayout {
.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
.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)
) {
if 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 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
};
for i in 0..len {
let position = (i as isize) - first_visible;
let left = area.left + (position as i32 * column_width);
let focused_idx = focused_idx as isize;
layouts.push(Rect {
left,
top: area.top,
right: column_width,
bottom: area.bottom,
});
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;
},
);
}
}
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
layouts
}
Self::BSP => recursive_fibonacci(
@@ -155,9 +166,10 @@ impl Arrangement for DefaultLayout {
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) && let 2.. = len
{
columns_reverse(&mut layouts);
) {
if let 2.. = len {
columns_reverse(&mut layouts);
}
}
layouts
@@ -179,9 +191,10 @@ impl Arrangement for DefaultLayout {
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) && let 2.. = len
{
rows_reverse(&mut layouts);
) {
if let 2.. = len {
rows_reverse(&mut layouts);
}
}
layouts
@@ -232,23 +245,25 @@ impl Arrangement for DefaultLayout {
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) && let 2.. = len
{
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
for rect in rest.iter_mut() {
rect.left = primary.left;
for rect in rest.iter_mut() {
rect.left = primary.left;
}
primary.left = rest[0].left + rest[0].right;
}
primary.left = rest[0].left + rest[0].right;
}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) && let 3.. = len
{
rows_reverse(&mut layouts[1..]);
) {
if let 3.. = len {
rows_reverse(&mut layouts[1..]);
}
}
layouts
@@ -302,23 +317,25 @@ impl Arrangement for DefaultLayout {
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) && let 2.. = len
{
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
primary.left = rest[0].left;
for rect in rest.iter_mut() {
rect.left = primary.left + primary.right;
primary.left = rest[0].left;
for rect in rest.iter_mut() {
rect.left = primary.left + primary.right;
}
}
}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) && let 3.. = len
{
rows_reverse(&mut layouts[1..]);
) {
if let 3.. = len {
rows_reverse(&mut layouts[1..]);
}
}
layouts
@@ -369,23 +386,25 @@ impl Arrangement for DefaultLayout {
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) && let 2.. = len
{
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
for rect in rest.iter_mut() {
rect.top = primary.top;
for rect in rest.iter_mut() {
rect.top = primary.top;
}
primary.top = rest[0].top + rest[0].bottom;
}
primary.top = rest[0].top + rest[0].bottom;
}
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) && let 3.. = len
{
columns_reverse(&mut layouts[1..]);
) {
if let 3.. = len {
columns_reverse(&mut layouts[1..]);
}
}
layouts
@@ -494,9 +513,10 @@ impl Arrangement for DefaultLayout {
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) && let 4.. = len
{
rows_reverse(&mut layouts[2..]);
) {
if let 4.. = len {
rows_reverse(&mut layouts[2..]);
}
}
layouts
@@ -514,25 +534,14 @@ impl Arrangement for DefaultLayout {
let len = len as i32;
let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
let num_cols = if let Some(rows) = row_constraint {
((len as f32) / (rows as f32)).ceil() as i32
} else {
(len as f32).sqrt().ceil() as i32
};
let num_cols = (len as f32).sqrt().ceil() as i32;
let mut iter = layouts.iter_mut().enumerate().peekable();
for col in 0..num_cols {
let iter_peek = iter.peek().map(|x| x.0).unwrap_or_default() as i32;
let remaining_windows = len - iter_peek;
let remaining_columns = num_cols - col;
let num_rows_in_this_col = if let Some(rows) = row_constraint {
(remaining_windows / remaining_columns).min(rows as i32)
} else {
remaining_windows / remaining_columns
};
let num_rows_in_this_col = remaining_windows / remaining_columns;
let win_height = area.bottom / num_rows_in_this_col;
let win_width = area.right / num_cols;
@@ -704,13 +713,9 @@ 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,
}
@@ -777,67 +782,67 @@ fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Optio
// This needs to be aware of layout flips
for (i, opt) in resize_dimensions.iter().enumerate() {
if let Some(resize_ref) = opt
&& i > 0
{
if resize_ref.left != 0 {
#[allow(clippy::if_not_else)]
let range = if i == 1 {
0..1
} else if i & 1 != 0 {
i - 1..i
} else {
i - 2..i
};
if let Some(resize_ref) = opt {
if i > 0 {
if resize_ref.left != 0 {
#[allow(clippy::if_not_else)]
let range = if i == 1 {
0..1
} else if i & 1 != 0 {
i - 1..i
} else {
i - 2..i
};
for n in range {
let should_adjust = n % 2 == 0;
if should_adjust {
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
adjacent_resize.right += resize_ref.left;
} else {
resize_adjustments[n] = Option::from(Rect {
left: 0,
top: 0,
right: resize_ref.left,
bottom: 0,
});
for n in range {
let should_adjust = n % 2 == 0;
if should_adjust {
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
adjacent_resize.right += resize_ref.left;
} else {
resize_adjustments[n] = Option::from(Rect {
left: 0,
top: 0,
right: resize_ref.left,
bottom: 0,
});
}
}
}
if let Some(rr) = resize_adjustments[i].as_mut() {
rr.left = 0;
}
}
if let Some(rr) = resize_adjustments[i].as_mut() {
rr.left = 0;
}
}
if resize_ref.top != 0 {
let range = if i == 1 {
0..1
} else if i & 1 == 0 {
i - 1..i
} else {
i - 2..i
};
if resize_ref.top != 0 {
let range = if i == 1 {
0..1
} else if i & 1 == 0 {
i - 1..i
} else {
i - 2..i
};
for n in range {
let should_adjust = n % 2 != 0;
if should_adjust {
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
adjacent_resize.bottom += resize_ref.top;
} else {
resize_adjustments[n] = Option::from(Rect {
left: 0,
top: 0,
right: 0,
bottom: resize_ref.top,
});
for n in range {
let should_adjust = n % 2 != 0;
if should_adjust {
if let Some(Some(adjacent_resize)) = resize_adjustments.get_mut(n) {
adjacent_resize.bottom += resize_ref.top;
} else {
resize_adjustments[n] = Option::from(Rect {
left: 0,
top: 0,
right: 0,
bottom: resize_ref.top,
});
}
}
}
}
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
resize.top = 0;
if let Some(Some(resize)) = resize_adjustments.get_mut(i) {
resize.top = 0;
}
}
}
}
@@ -922,7 +927,7 @@ fn recursive_fibonacci(
right: resized.right,
bottom: resized.bottom,
}]
} else if !idx.is_multiple_of(2) {
} else if idx % 2 != 0 {
let mut res = vec![Rect {
left: resized.left,
top: main_y,

View File

@@ -1,7 +1,7 @@
use crate::config_generation::ApplicationConfiguration;
use crate::config_generation::ApplicationOptions;
use crate::config_generation::MatchingRule;
use color_eyre::eyre;
use color_eyre::Result;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
@@ -36,12 +36,12 @@ impl DerefMut for ApplicationSpecificConfiguration {
}
impl ApplicationSpecificConfiguration {
pub fn load(pathbuf: &PathBuf) -> eyre::Result<Self> {
pub fn load(pathbuf: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(pathbuf)?;
Ok(serde_json::from_str(&content)?)
}
pub fn format(pathbuf: &PathBuf) -> eyre::Result<String> {
pub fn format(pathbuf: &PathBuf) -> Result<String> {
Ok(serde_json::to_string_pretty(&Self::load(pathbuf)?)?)
}
}

View File

@@ -1,5 +1,5 @@
use clap::ValueEnum;
use color_eyre::eyre;
use color_eyre::Result;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -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,
}
@@ -165,11 +142,11 @@ impl ApplicationConfiguration {
pub struct ApplicationConfigurationGenerator;
impl ApplicationConfigurationGenerator {
pub fn load(content: &str) -> eyre::Result<Vec<ApplicationConfiguration>> {
pub fn load(content: &str) -> Result<Vec<ApplicationConfiguration>> {
Ok(serde_yaml::from_str(content)?)
}
pub fn format(content: &str) -> eyre::Result<String> {
pub fn format(content: &str) -> Result<String> {
let mut cfgen = Self::load(content)?;
for cfg in &mut cfgen {
cfg.populate_default_matching_strategies();
@@ -179,10 +156,7 @@ impl ApplicationConfigurationGenerator {
Ok(serde_yaml::to_string(&cfgen)?)
}
fn merge(
base_content: &str,
override_content: &str,
) -> eyre::Result<Vec<ApplicationConfiguration>> {
fn merge(base_content: &str, override_content: &str) -> Result<Vec<ApplicationConfiguration>> {
let base_cfgen = Self::load(base_content)?;
let override_cfgen = Self::load(override_content)?;
@@ -208,7 +182,7 @@ impl ApplicationConfigurationGenerator {
pub fn generate_pwsh(
base_content: &str,
override_content: Option<&str>,
) -> eyre::Result<Vec<String>> {
) -> Result<Vec<String>> {
let mut cfgen = if let Some(override_content) = override_content {
Self::merge(base_content, override_content)?
} else {
@@ -259,10 +233,7 @@ impl ApplicationConfigurationGenerator {
Ok(lines)
}
pub fn generate_ahk(
base_content: &str,
override_content: Option<&str>,
) -> eyre::Result<Vec<String>> {
pub fn generate_ahk(base_content: &str, override_content: Option<&str>) -> Result<Vec<String>> {
let mut cfgen = if let Some(override_content) = override_content {
Self::merge(base_content, override_content)?
} else {

View File

@@ -1,7 +1,3 @@
use color_eyre::eyre;
use color_eyre::eyre::bail;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
@@ -9,6 +5,12 @@ use std::ops::Deref;
use std::ops::DerefMut;
use std::path::Path;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use serde::Deserialize;
use serde::Serialize;
use super::Rect;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -30,7 +32,7 @@ impl DerefMut for CustomLayout {
}
impl CustomLayout {
pub fn from_path<P: AsRef<Path>>(path: P) -> eyre::Result<Self> {
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let layout: Self = match path.extension() {
Some(extension) if extension == "yaml" || extension == "yml" => {
@@ -39,7 +41,7 @@ impl CustomLayout {
Some(extension) if extension == "json" => {
serde_json::from_reader(BufReader::new(File::open(path)?))?
}
_ => bail!("custom layouts must be json or yaml files"),
_ => return Err(anyhow!("custom layouts must be json or yaml files")),
};
if !layout.is_valid() {

View File

@@ -12,132 +12,31 @@ use super::Sizing;
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// A predefined komorebi layout
pub enum DefaultLayout {
/// BSP Layout
///
/// ```text
/// +-------+-----+
/// | | |
/// | +--+--+
/// | | |--|
/// +-------+--+--+
/// ```
BSP,
/// Columns Layout
///
/// ```text
/// +--+--+--+--+
/// | | | | |
/// | | | | |
/// | | | | |
/// +--+--+--+--+
/// ```
Columns,
/// Rows Layout
///
/// ```text
/// +-----------+
/// |-----------|
/// |-----------|
/// |-----------|
/// +-----------+
/// ```
Rows,
/// Vertical Stack Layout
///
/// ```text
/// +-------+-----+
/// | | |
/// | +-----+
/// | | |
/// +-------+-----+
/// ```
VerticalStack,
/// Horizontal Stack Layout
///
/// ```text
/// +------+------+
/// | |
/// |------+------+
/// | | |
/// +------+------+
/// ```
HorizontalStack,
/// Ultrawide Vertical Stack Layout
///
/// ```text
/// +-----+-----------+-----+
/// | | | |
/// | | +-----+
/// | | | |
/// | | +-----+
/// | | | |
/// +-----+-----------+-----+
/// ```
UltrawideVerticalStack,
/// Grid Layout
///
/// ```text
/// +-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
/// | | | | | | | | | | | | | | |
/// | | | | | | | | | | | | | +---+
/// +-----+-----+ | +---+---+ +---+---+---+ +---+---| |
/// | | | | | | | | | | | | | +---+
/// | | | | | | | | | | | | | | |
/// +-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
/// 4 windows 5 windows 6 windows 7 windows
/// ```
Grid,
/// Right Main Vertical Stack Layout
///
/// ```text
/// +-----+-------+
/// | | |
/// +-----+ |
/// | | |
/// +-----+-------+
/// ```
RightMainVerticalStack,
/// Scrolling Layout
///
/// ```text
/// +--+--+--+--+--+--+
/// | | | |
/// | | | |
/// | | | |
/// +--+--+--+--+--+--+
/// ```
Scrolling,
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Options for specific layouts
pub struct LayoutOptions {
/// Options related to the Scrolling layout
pub scrolling: Option<ScrollingLayoutOptions>,
/// Options related to the Grid layout
pub grid: Option<GridLayoutOptions>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Options for the Scrolling layout
pub struct ScrollingLayoutOptions {
/// Desired number of visible columns (default: 3)
pub columns: usize,
/// With an odd number of visible columns, keep the focused window column centered
pub center_focused_column: Option<bool>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Options for the Grid layout
pub struct GridLayoutOptions {
/// Maximum number of rows per grid column
pub rows: usize,
}
impl DefaultLayout {

View File

@@ -1,10 +1,9 @@
use super::DefaultLayout;
use super::OperationDirection;
use super::custom_layout::Column;
use super::custom_layout::ColumnSplit;
use super::custom_layout::ColumnSplitWithCapacity;
use super::custom_layout::CustomLayout;
use crate::default_layout::LayoutOptions;
use super::DefaultLayout;
use super::OperationDirection;
pub trait Direction {
fn index_in_direction(
@@ -12,7 +11,6 @@ pub trait Direction {
op_direction: OperationDirection,
idx: usize,
count: usize,
layout_options: Option<LayoutOptions>,
) -> Option<usize>;
fn is_valid_direction(
@@ -20,35 +18,30 @@ pub trait Direction {
op_direction: OperationDirection,
idx: usize,
count: usize,
layout_options: Option<LayoutOptions>,
) -> bool;
fn up_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize;
fn down_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize;
fn left_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize;
fn right_index(
&self,
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize;
}
@@ -58,53 +51,32 @@ impl Direction for DefaultLayout {
op_direction: OperationDirection,
idx: usize,
count: usize,
layout_options: Option<LayoutOptions>,
) -> Option<usize> {
match op_direction {
OperationDirection::Left => {
if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.left_index(
Some(op_direction),
idx,
Some(count),
layout_options,
))
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.left_index(Some(op_direction), idx, Some(count)))
} else {
None
}
}
OperationDirection::Right => {
if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.right_index(
Some(op_direction),
idx,
Some(count),
layout_options,
))
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.right_index(Some(op_direction), idx, Some(count)))
} else {
None
}
}
OperationDirection::Up => {
if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.up_index(
Some(op_direction),
idx,
Some(count),
layout_options,
))
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.up_index(Some(op_direction), idx, Some(count)))
} else {
None
}
}
OperationDirection::Down => {
if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.down_index(
Some(op_direction),
idx,
Some(count),
layout_options,
))
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.down_index(Some(op_direction), idx, Some(count)))
} else {
None
}
@@ -117,7 +89,6 @@ impl Direction for DefaultLayout {
op_direction: OperationDirection,
idx: usize,
count: usize,
layout_options: Option<LayoutOptions>,
) -> bool {
if count < 2 {
return false;
@@ -130,17 +101,17 @@ impl Direction for DefaultLayout {
Self::Rows | Self::HorizontalStack => idx != 0,
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx > 2,
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
Self::Grid => !is_grid_edge(op_direction, idx, count),
Self::Scrolling => false,
},
OperationDirection::Down => match self {
Self::BSP => idx != count - 1 && !idx.is_multiple_of(2),
Self::BSP => idx != count - 1 && idx % 2 != 0,
Self::Columns => false,
Self::Rows => idx != count - 1,
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
Self::HorizontalStack => idx == 0,
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
Self::Grid => !is_grid_edge(op_direction, idx, count),
Self::Scrolling => false,
},
OperationDirection::Left => match self {
@@ -150,11 +121,11 @@ impl Direction for DefaultLayout {
Self::Rows => false,
Self::HorizontalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx != 1,
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
Self::Grid => !is_grid_edge(op_direction, idx, count),
Self::Scrolling => idx != 0,
},
OperationDirection::Right => match self {
Self::BSP => idx.is_multiple_of(2) && idx != count - 1,
Self::BSP => idx % 2 == 0 && idx != count - 1,
Self::Columns => idx != count - 1,
Self::Rows => false,
Self::VerticalStack => idx == 0,
@@ -164,7 +135,7 @@ impl Direction for DefaultLayout {
2 => idx != 0,
_ => idx < 2,
},
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
Self::Grid => !is_grid_edge(op_direction, idx, count),
Self::Scrolling => idx != count - 1,
},
}
@@ -175,11 +146,10 @@ impl Direction for DefaultLayout {
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize {
match self {
Self::BSP => {
if idx.is_multiple_of(2) {
if idx % 2 == 0 {
idx - 1
} else {
idx - 2
@@ -191,7 +161,7 @@ impl Direction for DefaultLayout {
| Self::UltrawideVerticalStack
| Self::RightMainVerticalStack => idx - 1,
Self::HorizontalStack => 0,
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
Self::Grid => grid_neighbor(op_direction, idx, count),
Self::Scrolling => unreachable!(),
}
}
@@ -201,7 +171,6 @@ impl Direction for DefaultLayout {
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize {
match self {
Self::BSP
@@ -211,7 +180,7 @@ impl Direction for DefaultLayout {
| Self::RightMainVerticalStack => idx + 1,
Self::Columns => unreachable!(),
Self::HorizontalStack => 1,
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
Self::Grid => grid_neighbor(op_direction, idx, count),
Self::Scrolling => unreachable!(),
}
}
@@ -221,11 +190,10 @@ impl Direction for DefaultLayout {
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize {
match self {
Self::BSP => {
if idx.is_multiple_of(2) {
if idx % 2 == 0 {
idx - 2
} else {
idx - 1
@@ -240,7 +208,7 @@ impl Direction for DefaultLayout {
1 => unreachable!(),
_ => 0,
},
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
Self::Grid => grid_neighbor(op_direction, idx, count),
Self::Scrolling => idx - 1,
}
}
@@ -250,7 +218,6 @@ impl Direction for DefaultLayout {
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize {
match self {
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
@@ -262,7 +229,7 @@ impl Direction for DefaultLayout {
0 => 2,
_ => unreachable!(),
},
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
Self::Grid => grid_neighbor(op_direction, idx, count),
Self::Scrolling => idx + 1,
}
}
@@ -293,32 +260,21 @@ struct GridTouchingEdges {
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
fn get_grid_item(idx: usize, count: usize, layout_options: Option<LayoutOptions>) -> GridItem {
let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
let num_cols = if let Some(rows) = row_constraint {
((count as f32) / (rows as f32)).ceil() as i32
} else {
(count as f32).sqrt().ceil() as i32
};
fn get_grid_item(idx: usize, count: usize) -> GridItem {
let num_cols = (count as f32).sqrt().ceil() as usize;
let mut iter = 0;
for col in 0..num_cols {
let remaining_windows = (count - iter) as i32;
let remaining_windows = count - iter;
let remaining_columns = num_cols - col;
let num_rows_in_this_col = if let Some(rows) = row_constraint {
(remaining_windows / remaining_columns).min(rows as i32)
} else {
remaining_windows / remaining_columns
};
let num_rows_in_this_col = remaining_windows / remaining_columns;
for row in 0..num_rows_in_this_col {
if iter == idx {
return GridItem {
state: GridItemState::Valid,
row: (row + 1) as usize,
num_rows: num_rows_in_this_col as usize,
row: row + 1,
num_rows: num_rows_in_this_col,
touching_edges: GridTouchingEdges {
left: col == 0,
right: col == num_cols - 1,
@@ -345,13 +301,8 @@ fn get_grid_item(idx: usize, count: usize, layout_options: Option<LayoutOptions>
}
}
fn is_grid_edge(
op_direction: OperationDirection,
idx: usize,
count: usize,
layout_options: Option<LayoutOptions>,
) -> bool {
let item = get_grid_item(idx, count, layout_options);
fn is_grid_edge(op_direction: OperationDirection, idx: usize, count: usize) -> bool {
let item = get_grid_item(idx, count);
match item.state {
GridItemState::Invalid => false,
@@ -368,7 +319,6 @@ fn grid_neighbor(
op_direction: Option<OperationDirection>,
idx: usize,
count: Option<usize>,
layout_options: Option<LayoutOptions>,
) -> usize {
let Some(op_direction) = op_direction else {
return 0;
@@ -378,11 +328,11 @@ fn grid_neighbor(
return 0;
};
let item = get_grid_item(idx, count, layout_options);
let item = get_grid_item(idx, count);
match op_direction {
OperationDirection::Left => {
let item_from_prev_col = get_grid_item(idx - item.row, count, layout_options);
let item_from_prev_col = get_grid_item(idx - item.row, count);
if item.touching_edges.up && item.num_rows != item_from_prev_col.num_rows {
return idx - (item.num_rows - 1);
@@ -406,42 +356,36 @@ impl Direction for CustomLayout {
op_direction: OperationDirection,
idx: usize,
count: usize,
layout_options: Option<LayoutOptions>,
) -> Option<usize> {
if count <= self.len() {
return DefaultLayout::Columns.index_in_direction(
op_direction,
idx,
count,
layout_options,
);
return DefaultLayout::Columns.index_in_direction(op_direction, idx, count);
}
match op_direction {
OperationDirection::Left => {
if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.left_index(None, idx, None, layout_options))
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.left_index(None, idx, None))
} else {
None
}
}
OperationDirection::Right => {
if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.right_index(None, idx, None, layout_options))
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.right_index(None, idx, None))
} else {
None
}
}
OperationDirection::Up => {
if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.up_index(None, idx, None, layout_options))
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.up_index(None, idx, None))
} else {
None
}
}
OperationDirection::Down => {
if self.is_valid_direction(op_direction, idx, count, layout_options) {
Option::from(self.down_index(None, idx, None, layout_options))
if self.is_valid_direction(op_direction, idx, count) {
Option::from(self.down_index(None, idx, None))
} else {
None
}
@@ -454,15 +398,9 @@ impl Direction for CustomLayout {
op_direction: OperationDirection,
idx: usize,
count: usize,
layout_options: Option<LayoutOptions>,
) -> bool {
if count <= self.len() {
return DefaultLayout::Columns.is_valid_direction(
op_direction,
idx,
count,
layout_options,
);
return DefaultLayout::Columns.is_valid_direction(op_direction, idx, count);
}
match op_direction {
@@ -506,7 +444,6 @@ impl Direction for CustomLayout {
_op_direction: Option<OperationDirection>,
idx: usize,
_count: Option<usize>,
_layout_options: Option<LayoutOptions>,
) -> usize {
idx - 1
}
@@ -516,7 +453,6 @@ impl Direction for CustomLayout {
_op_direction: Option<OperationDirection>,
idx: usize,
_count: Option<usize>,
_layout_options: Option<LayoutOptions>,
) -> usize {
idx + 1
}
@@ -526,7 +462,6 @@ impl Direction for CustomLayout {
_op_direction: Option<OperationDirection>,
idx: usize,
_count: Option<usize>,
_layout_options: Option<LayoutOptions>,
) -> usize {
let column_idx = self.column_for_container_idx(idx);
if column_idx - 1 == 0 {
@@ -541,7 +476,6 @@ impl Direction for CustomLayout {
_op_direction: Option<OperationDirection>,
idx: usize,
_count: Option<usize>,
_layout_options: Option<LayoutOptions>,
) -> usize {
let column_idx = self.column_for_container_idx(idx);
self.first_container_idx(column_idx + 1)

View File

@@ -1,20 +1,19 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
#![allow(deprecated)] // allow deprecated variants like HidingBehaviour::Hide to be used in derive macros
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::str::FromStr;
use clap::ValueEnum;
use color_eyre::eyre;
use color_eyre::Result;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
use crate::KomorebiTheme;
use crate::animation::prefix::AnimationPrefix;
use crate::KomorebiTheme;
pub use animation::AnimationStyle;
pub use arrangement::Arrangement;
pub use arrangement::Axis;
@@ -24,14 +23,14 @@ pub use custom_layout::ColumnSplitWithCapacity;
pub use custom_layout::ColumnWidth;
pub use custom_layout::CustomLayout;
pub use cycle_direction::CycleDirection;
pub use default_layout::*;
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 pathext::PathExt;
pub use pathext::ResolvedPathBuf;
pub use rect::Rect;
pub mod animation;
@@ -56,8 +55,6 @@ pub enum SocketMessage {
// Window / Container Commands
FocusWindow(OperationDirection),
MoveWindow(OperationDirection),
PreselectDirection(OperationDirection),
CancelPreselect,
CycleFocusWindow(CycleDirection),
CycleMoveWindow(CycleDirection),
StackWindow(OperationDirection),
@@ -90,7 +87,6 @@ pub enum SocketMessage {
Close,
Minimize,
Promote,
PromoteSwap,
PromoteFocus,
PromoteWindow(OperationDirection),
EagerFocus(String),
@@ -206,7 +202,6 @@ pub enum SocketMessage {
StackbarFontFamily(Option<String>),
WorkAreaOffset(Rect),
MonitorWorkAreaOffset(usize, Rect),
WorkspaceWorkAreaOffset(usize, usize, Rect),
ToggleWindowBasedWorkAreaOffset,
ResizeDelta(i32),
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
@@ -252,7 +247,7 @@ pub enum SocketMessage {
}
impl SocketMessage {
pub fn as_bytes(&self) -> eyre::Result<Vec<u8>> {
pub fn as_bytes(&self) -> Result<Vec<u8>> {
Ok(serde_json::to_string(self)?.as_bytes().to_vec())
}
}
@@ -260,7 +255,7 @@ impl SocketMessage {
impl FromStr for SocketMessage {
type Err = serde_json::Error;
fn from_str(s: &str) -> eyre::Result<Self, Self::Err> {
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
@@ -274,24 +269,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,
}
@@ -299,7 +287,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
@@ -314,7 +301,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
@@ -338,20 +324,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,
}
@@ -372,37 +351,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
@@ -421,7 +393,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,
}
@@ -429,7 +401,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]
@@ -442,7 +413,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]
@@ -455,7 +425,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]
@@ -495,7 +464,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]
@@ -510,7 +478,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,
@@ -521,12 +488,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,
@@ -536,22 +501,18 @@ 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))]
/// Sizing
pub enum Sizing {
/// Increase
Increase,
/// Decrease
Decrease,
}
@@ -575,12 +536,9 @@ impl Sizing {
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,
}
@@ -591,9 +549,7 @@ mod tests {
#[test]
fn deserializes() {
// Set a variable for testing
unsafe {
std::env::set_var("VAR", "VALUE");
}
std::env::set_var("VAR", "VALUE");
let json = r#"{"type":"WorkspaceLayoutCustomRule","content":[0,0,0,"/path/%VAR%/d"]}"#;
let message: SocketMessage = serde_json::from_str(json).unwrap();

View File

@@ -1,14 +1,14 @@
use std::num::NonZeroUsize;
use super::Axis;
use super::direction::Direction;
use crate::default_layout::LayoutOptions;
use clap::ValueEnum;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
use super::direction::Direction;
use super::Axis;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum OperationDirection {
@@ -57,8 +57,7 @@ impl OperationDirection {
layout_flip: Option<Axis>,
idx: usize,
len: NonZeroUsize,
layout_options: Option<LayoutOptions>,
) -> Option<usize> {
layout.index_in_direction(self.flip(layout_flip), idx, len.get(), layout_options)
layout.index_in_direction(self.flip(layout_flip), idx, len.get())
}
}

View File

@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::env;
use std::ffi::OsStr;
use std::path::Component;
use std::path::Path;
@@ -57,7 +58,7 @@ impl<P: AsRef<Path>> PathExt for P {
// if component is a variable, get the value from the environment
if let Some(var) = var {
let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
if let Some(value) = std::env::var_os(var) {
if let Some(value) = env::var_os(var) {
out.push(value);
continue;
}
@@ -121,16 +122,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(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
<PathBuf as schemars::JsonSchema>::json_schema(gen)
}
}
@@ -167,9 +165,7 @@ mod tests {
#[test]
fn resolves_env_vars() {
// Set a variable for testing
unsafe {
std::env::set_var("VAR", "VALUE");
}
std::env::set_var("VAR", "VALUE");
// %VAR% format
assert_eq!(resolve("/path/%VAR%/d"), expected("/path/VALUE/d"));
@@ -187,9 +183,7 @@ mod tests {
assert_eq!(resolve("/path/$ASD/to/d"), expected("/path/$ASD/to/d"));
// Set a $env:USERPROFILE variable for testing
unsafe {
std::env::set_var("USERPROFILE", "C:\\Users\\user");
}
std::env::set_var("USERPROFILE", "C:\\Users\\user");
// ~ and $HOME should be replaced with $Env:USERPROFILE
assert_eq!(resolve("~"), expected("C:\\Users\\user"));

View File

@@ -4,15 +4,14 @@ use windows::Win32::Foundation::RECT;
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Rectangle dimensions
pub struct Rect {
/// 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,
}

View File

@@ -44,15 +44,13 @@ pub fn send_notification(hwnd: isize) {
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || {
loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});

View File

@@ -8,7 +8,7 @@ pub mod ring;
pub mod container;
pub mod core;
pub mod focus_manager;
pub mod lockable_sequence;
pub mod locked_deque;
pub mod monitor;
pub mod monitor_reconciliator;
pub mod process_command;
@@ -16,9 +16,7 @@ pub mod process_event;
pub mod process_movement;
pub mod reaper;
pub mod set_window_position;
pub mod splash;
pub mod stackbar_manager;
pub mod state;
pub mod static_config;
pub mod styles;
pub mod theme_manager;
@@ -41,12 +39,12 @@ use std::io::Write;
use std::net::TcpStream;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub use core::*;
pub use komorebi_themes::colour::*;
@@ -64,7 +62,7 @@ use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::core::config_generation::WorkspaceMatchingRule;
use color_eyre::eyre;
use color_eyre::Result;
use crossbeam_utils::atomic::AtomicCell;
use os_info::Version;
use parking_lot::Mutex;
@@ -72,11 +70,10 @@ use parking_lot::RwLock;
use regex::Regex;
use serde::Deserialize;
use serde::Serialize;
use state::State;
use uds_windows::UnixStream;
use which::which;
use winreg::RegKey;
use winreg::enums::HKEY_CURRENT_USER;
use winreg::RegKey;
lazy_static! {
static ref HIDDEN_HWNDS: Arc<Mutex<Vec<isize>>> = Arc::new(Mutex::new(vec![]));
@@ -203,7 +200,8 @@ lazy_static! {
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
@@ -214,10 +212,11 @@ lazy_static! {
pub static ref AHK_EXE: String = {
let mut ahk: String = String::from("autohotkey.exe");
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE")
&& which(&komorebi_ahk_exe).is_ok() {
if let Ok(komorebi_ahk_exe) = std::env::var("KOMOREBI_AHK_EXE") {
if which(&komorebi_ahk_exe).is_ok() {
ahk = komorebi_ahk_exe;
}
}
ahk
};
@@ -242,9 +241,7 @@ lazy_static! {
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);
@@ -258,29 +255,6 @@ pub static WINDOW_HANDLING_BEHAVIOUR: AtomicCell<WindowHandlingBehaviour> =
shadow_rs::shadow!(build);
pub const PUBLIC_KEY: [u8; 32] = [
0x5a, 0x69, 0x4a, 0xe1, 0x3c, 0x4b, 0xc8, 0x4e, 0xc3, 0x79, 0x0f, 0xab, 0x27, 0x6b, 0x7e, 0xdd,
0x6b, 0x39, 0x6f, 0xa2, 0xc3, 0x9f, 0x3d, 0x48, 0xf2, 0x72, 0x56, 0x41, 0x1b, 0xc8, 0x08, 0xdb,
];
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
pub struct License {
#[serde(rename = "hasValidSubscription")]
pub has_valid_subscription: bool,
pub timestamp: i64,
#[serde(rename = "currentEndPeriod")]
pub current_end_period: Option<i64>,
pub signature: String,
}
/// A trait for types that can be marked as locked or unlocked.
pub trait Lockable {
/// Returns `true` if the item is locked.
fn locked(&self) -> bool;
/// Sets the locked state of the item.
fn set_locked(&mut self, locked: bool) -> &mut Self;
}
#[must_use]
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
@@ -325,7 +299,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
current
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum NotificationEvent {
@@ -342,17 +316,14 @@ pub enum VirtualDesktopNotification {
LeftAssociatedVirtualDesktop,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Notification {
pub event: NotificationEvent,
pub state: State,
}
pub fn notify_subscribers(
notification: Notification,
state_has_been_modified: bool,
) -> eyre::Result<()> {
pub fn notify_subscribers(notification: Notification, state_has_been_modified: bool) -> Result<()> {
let is_override_event = matches!(
notification.event,
NotificationEvent::Socket(SocketMessage::AddSubscriberSocket(_))
@@ -433,7 +404,7 @@ pub fn notify_subscribers(
Ok(())
}
pub fn load_configuration() -> eyre::Result<()> {
pub fn load_configuration() -> Result<()> {
let config_pwsh = HOME_DIR.join("komorebi.ps1");
let config_ahk = HOME_DIR.join("komorebi.ahk");

View File

@@ -1,357 +0,0 @@
use std::collections::VecDeque;
use crate::Lockable;
/// A sequence supporting insertion, removal, and swapping of elements while preserving the absolute
/// positions of locked items.
pub trait LockableSequence<T: Lockable> {
/// Inserts a value at `idx`, keeping locked elements at their absolute positions.
fn insert_respecting_locks(&mut self, idx: usize, value: T) -> usize;
/// Removes the element at `idx`, keeping locked elements at their absolute positions.
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T>;
/// Swaps the elements at indices `i` and `j`, keeping locked elements at their absolute positions.
fn swap_respecting_locks(&mut self, i: usize, j: usize);
}
impl<T: Lockable> LockableSequence<T> for VecDeque<T> {
/// Insert `value` at logical index `idx`, with trying to keep locked elements
/// (`is_locked()`) anchored at their original positions.
///
/// Returns the final index of the inserted element.
fn insert_respecting_locks(&mut self, mut idx: usize, value: T) -> usize {
// 1. Bounds check: if index is out of range, simply append.
if idx >= self.len() {
self.push_back(value);
return self.len() - 1; // last index
}
// 2. Normal VecDeque insertion
self.insert(idx, value);
// 3. Walk left-to-right once, swapping any misplaced locked element. After
// the VecDeque::insert all items after `idx` have moved right by one. For every locked
// element that is now to the right of an unlocked one, swap it back left exactly once.
for index in (idx + 1)..self.len() {
if self[index].locked() && !self[index - 1].locked() {
self.swap(index - 1, index);
// If the element we just inserted participated in the swap,
// update `idx` so we can return its final location.
if idx == index - 1 {
idx = index;
}
}
}
idx
}
/// Remove element at `idx`, with trying to keep locked elements
/// (`is_locked()`) anchored at their original positions.
///
/// Returns the removed element, or `None` if `idx` is out of bounds.
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T> {
// 1. Bounds check: if index is out of range, do nothing.
if idx >= self.len() {
return None;
}
// 2. Remove the element at the requested index.
// All elements after idx are now shifted left by 1.
let removed = self.remove(idx)?;
// 3. If less than 2 elements remain, nothing to shift.
if self.len() < 2 {
return Some(removed);
}
// 4. Iterate from the element just after the removed spot up to the second-to-last
// element, right-to-left. This loop "fixes" locked elements that were shifted left
// off their anchored positions: If a locked element now has an unlocked element
// to its right, swap them back to restore locked order.
for index in (idx..self.len() - 1).rev() {
// If current is locked and the next one is not locked, swap them.
if self[index].locked() && !self[index + 1].locked() {
self.swap(index, index + 1);
}
}
// 5. Return the removed value.
Some(removed)
}
/// Swaps the elements at indices `i` and `j`, along with their `locked` status, ensuring
/// the lock state remains associated with the position rather than the element itself.
fn swap_respecting_locks(&mut self, i: usize, j: usize) {
self.swap(i, j);
let locked_i = self[i].locked();
let locked_j = self[j].locked();
self[i].set_locked(locked_j);
self[j].set_locked(locked_i);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
struct TestItem {
val: i32,
locked: bool,
}
impl Lockable for TestItem {
fn locked(&self) -> bool {
self.locked
}
fn set_locked(&mut self, locked: bool) -> &mut Self {
self.locked = locked;
self
}
}
fn vals(v: &VecDeque<TestItem>) -> Vec<i32> {
v.iter().map(|x| x.val).collect()
}
fn test_deque(items: &[(i32, bool)]) -> VecDeque<TestItem> {
items
.iter()
.cloned()
.map(|(val, locked)| TestItem { val, locked })
.collect()
}
#[test]
fn test_insert_respecting_locks() {
// Test case 1: Basic insertion with locked index
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
// Insert at index 0, should shift elements while keeping index 2 locked
ring.insert_respecting_locks(
0,
TestItem {
val: 99,
locked: false,
},
);
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
assert_eq!(vals(&ring), vec![99, 0, 2, 1, 3, 4]);
}
// Test case 2: Insert at a locked index (should insert after locked)
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
// Try to insert at locked index 2, should insert at index 3 instead
let actual_index = ring.insert_respecting_locks(
2,
TestItem {
val: 99,
locked: false,
},
);
assert_eq!(actual_index, 3);
assert_eq!(vals(&ring), vec![0, 1, 2, 99, 3, 4]);
}
// Test case 3: Multiple locked indices
{
// Lock index 1 and 3
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
// Insert at index 0, should maintain locked indices
ring.insert_respecting_locks(
0,
TestItem {
val: 99,
locked: false,
},
);
// Elements '1' and '3' remain at indices 1 and 3
assert_eq!(vals(&ring), vec![99, 1, 0, 3, 2, 4]);
}
// Test case 4: Insert at end
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
let actual_index = ring.insert_respecting_locks(
5,
TestItem {
val: 99,
locked: false,
},
);
assert_eq!(actual_index, 5);
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
}
// Test case 5: Empty ring
{
let mut ring = test_deque(&[]);
// Insert into empty deque
let actual_index = ring.insert_respecting_locks(
0,
TestItem {
val: 99,
locked: false,
},
);
assert_eq!(actual_index, 0);
assert_eq!(vals(&ring), vec![99]);
}
// Test case 6: All indices locked
{
// Lock all indices
let mut ring = test_deque(&[(0, true), (1, true), (2, true), (3, true), (4, true)]);
// Try to insert at index 2, should insert at the end
let actual_index = ring.insert_respecting_locks(
2,
TestItem {
val: 99,
locked: false,
},
);
assert_eq!(actual_index, 5);
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
}
// Test case 7: Consecutive locked indices
{
// Lock index 2 and 3
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, true), (4, false)]);
// Insert at index 1, should maintain consecutive locked indices
ring.insert_respecting_locks(
1,
TestItem {
val: 99,
locked: false,
},
);
// Elements '2' and '3' remain at indices 2 and 3
assert_eq!(vals(&ring), vec![0, 99, 2, 3, 1, 4]);
}
}
#[test]
fn test_remove_respecting_locks() {
// Test case 1: Remove a non-locked index before a locked index
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
let removed = ring.remove_respecting_locks(0);
assert_eq!(removed.map(|x| x.val), Some(0));
// Elements '2' remain at index 2
assert_eq!(vals(&ring), vec![1, 3, 2, 4]);
}
// Test case 2: Remove a locked index
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
let removed = ring.remove_respecting_locks(2);
assert_eq!(removed.map(|x| x.val), Some(2));
// Elements should stay at the same places
assert_eq!(vals(&ring), vec![0, 1, 3, 4]);
}
// Test case 3: Remove an index after a locked index
{
// Lock index 1
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false), (4, false)]);
let removed = ring.remove_respecting_locks(3);
assert_eq!(removed.map(|x| x.val), Some(3));
// Elements should stay at the same places
assert_eq!(vals(&ring), vec![0, 1, 2, 4]);
}
// Test case 4: Multiple locked indices
{
// Lock index 1 and 3
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
let removed = ring.remove_respecting_locks(0);
assert_eq!(removed.map(|x| x.val), Some(0));
// Elements '1' and '3' remain at indices '1' and '3'
assert_eq!(vals(&ring), vec![2, 1, 4, 3]);
}
// Test case 5: Remove the last element
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
let removed = ring.remove_respecting_locks(4);
assert_eq!(removed.map(|x| x.val), Some(4));
// Index 2 should still be at the same place
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
}
// Test case 6: Invalid index
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
let removed = ring.remove_respecting_locks(10);
assert_eq!(removed, None);
// Deque unchanged
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4]);
}
// Test case 7: Remove enough elements to make a locked index invalid
{
// Lock index 2
let mut ring = test_deque(&[(0, false), (1, false), (2, true)]);
ring.remove_respecting_locks(0);
// Index 2 should now be '1'
assert_eq!(vals(&ring), vec![1, 2]);
}
// Test case 8: Removing an element before multiple locked indices
{
// Lock index 2 and 4
let mut ring = test_deque(&[
(0, false),
(1, false),
(2, true),
(3, false),
(4, true),
(5, false),
]);
let removed = ring.remove_respecting_locks(1);
assert_eq!(removed.map(|x| x.val), Some(1));
// Both indices should still be at the same place
assert_eq!(vals(&ring), vec![0, 3, 2, 5, 4]);
}
}
#[test]
fn test_swap_respecting_locks_various_cases() {
// Swap unlocked and locked
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false)]);
ring.swap_respecting_locks(0, 1);
assert_eq!(vals(&ring), vec![1, 0, 2, 3]);
assert!(!ring[0].locked);
assert!(ring[1].locked);
ring.swap_respecting_locks(0, 1);
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
assert!(!ring[0].locked);
assert!(ring[1].locked);
// Both locked
let mut ring = test_deque(&[(0, true), (1, false), (2, true)]);
ring.swap_respecting_locks(0, 2);
assert_eq!(vals(&ring), vec![2, 1, 0]);
assert!(ring[0].locked);
assert!(!ring[1].locked);
assert!(ring[2].locked);
// Both unlocked
let mut ring = test_deque(&[(0, false), (1, true), (2, false)]);
ring.swap_respecting_locks(0, 2);
assert_eq!(vals(&ring), vec![2, 1, 0]);
assert!(!ring[0].locked);
assert!(ring[1].locked);
assert!(!ring[2].locked);
}
}

View File

@@ -0,0 +1,316 @@
use std::collections::BTreeSet;
use std::collections::VecDeque;
pub struct LockedDeque<'a, T> {
deque: &'a mut VecDeque<T>,
locked_indices: &'a mut BTreeSet<usize>,
}
impl<'a, T: PartialEq> LockedDeque<'a, T> {
pub fn new(deque: &'a mut VecDeque<T>, locked_indices: &'a mut BTreeSet<usize>) -> Self {
Self {
deque,
locked_indices,
}
}
pub fn insert(&mut self, index: usize, value: T) -> usize {
insert_respecting_locks(self.deque, self.locked_indices, index, value)
}
pub fn remove(&mut self, index: usize) -> Option<T> {
remove_respecting_locks(self.deque, self.locked_indices, index)
}
}
pub fn insert_respecting_locks<T>(
deque: &mut VecDeque<T>,
locked_idx: &mut BTreeSet<usize>,
idx: usize,
value: T,
) -> usize {
if idx == deque.len() {
deque.push_back(value);
return idx;
}
let mut new_deque = VecDeque::with_capacity(deque.len() + 1);
let mut temp_locked_deque = VecDeque::new();
let mut j = 0;
let mut corrected_idx = idx;
for (i, el) in deque.drain(..).enumerate() {
if i == idx {
corrected_idx = j;
}
if locked_idx.contains(&i) {
temp_locked_deque.push_back(el);
} else {
new_deque.push_back(el);
j += 1;
}
}
new_deque.insert(corrected_idx, value);
for (locked_el, locked_idx) in temp_locked_deque.into_iter().zip(locked_idx.iter()) {
new_deque.insert(*locked_idx, locked_el);
if *locked_idx <= corrected_idx {
corrected_idx += 1;
}
}
*deque = new_deque;
corrected_idx
}
pub fn remove_respecting_locks<T>(
deque: &mut VecDeque<T>,
locked_idx: &mut BTreeSet<usize>,
idx: usize,
) -> Option<T> {
if idx >= deque.len() {
return None;
}
let final_size = deque.len() - 1;
let mut new_deque = VecDeque::with_capacity(final_size);
let mut temp_locked_deque = VecDeque::new();
let mut removed = None;
let mut removed_locked_idx = None;
for (i, el) in deque.drain(..).enumerate() {
if i == idx {
removed = Some(el);
removed_locked_idx = locked_idx.contains(&i).then_some(i);
} else if locked_idx.contains(&i) {
temp_locked_deque.push_back(el);
} else {
new_deque.push_back(el);
}
}
if let Some(i) = removed_locked_idx {
let mut above = locked_idx.split_off(&i);
above.pop_first();
locked_idx.extend(above.into_iter().map(|i| i - 1));
}
while locked_idx.last().is_some_and(|i| *i >= final_size) {
locked_idx.pop_last();
}
let extra_invalid_idx = (new_deque.len()
..(new_deque.len() + temp_locked_deque.len() - locked_idx.len()))
.collect::<Vec<_>>();
for (locked_el, locked_idx) in temp_locked_deque
.into_iter()
.zip(locked_idx.iter().chain(extra_invalid_idx.iter()))
{
new_deque.insert(*locked_idx, locked_el);
}
*deque = new_deque;
removed
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
use std::collections::VecDeque;
#[test]
fn test_insert_respecting_locks() {
// Test case 1: Basic insertion with locked index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
// Insert at index 0, should shift elements while keeping index 2 locked
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
assert_eq!(deque, VecDeque::from(vec![99, 0, 2, 1, 3, 4]));
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
}
// Test case 2: Insert at a locked index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
// Try to insert at locked index 2, should insert at index 3 instead
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
assert_eq!(actual_index, 3);
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 99, 3, 4]));
}
// Test case 3: Multiple locked indices
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(1); // Lock index 1
locked.insert(3); // Lock index 3
// Insert at index 0, should maintain locked indices
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
assert_eq!(deque, VecDeque::from(vec![99, 1, 0, 3, 2, 4]));
// Elements '1' and '3' remain at indices 1 and 3
}
// Test case 4: Insert at end
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
// Insert at end of deque
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 5, 99);
assert_eq!(actual_index, 5);
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
}
// Test case 5: Empty deque
{
let mut deque = VecDeque::new();
let mut locked = BTreeSet::new();
// Insert into empty deque
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 0, 99);
assert_eq!(actual_index, 0);
assert_eq!(deque, VecDeque::from(vec![99]));
}
// Test case 6: All indices locked
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
for i in 0..5 {
locked.insert(i); // Lock all indices
}
// Try to insert at index 2, should insert at the end
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
assert_eq!(actual_index, 5);
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
}
// Test case 7: Consecutive locked indices
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
locked.insert(3); // Lock index 3
// Insert at index 1, should maintain consecutive locked indices
insert_respecting_locks(&mut deque, &mut locked, 1, 99);
assert_eq!(deque, VecDeque::from(vec![0, 99, 2, 3, 1, 4]));
// Elements '2' and '3' remain at indices 2 and 3
}
}
#[test]
fn test_remove_respecting_locks() {
// Test case 1: Remove a non-locked index before a locked index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
assert_eq!(removed, Some(0));
assert_eq!(deque, VecDeque::from(vec![1, 3, 2, 4]));
assert!(locked.contains(&2)); // Index 2 should still be locked
}
// Test case 2: Remove a locked index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
let removed = remove_respecting_locks(&mut deque, &mut locked, 2);
assert_eq!(removed, Some(2));
assert_eq!(deque, VecDeque::from(vec![0, 1, 3, 4]));
assert!(!locked.contains(&2)); // Index 2 should be unlocked
}
// Test case 3: Remove an index after a locked index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(1); // Lock index 1
let removed = remove_respecting_locks(&mut deque, &mut locked, 3);
assert_eq!(removed, Some(3));
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 4]));
assert!(locked.contains(&1)); // Index 1 should still be locked
}
// Test case 4: Multiple locked indices
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(1); // Lock index 1
locked.insert(3); // Lock index 3
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
assert_eq!(removed, Some(0));
assert_eq!(deque, VecDeque::from(vec![2, 1, 4, 3]));
assert!(locked.contains(&1) && locked.contains(&3)); // Both indices should still be locked
}
// Test case 5: Remove the last element
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
let removed = remove_respecting_locks(&mut deque, &mut locked, 4);
assert_eq!(removed, Some(4));
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3]));
assert!(locked.contains(&2)); // Index 2 should still be locked
}
// Test case 6: Invalid index
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
let removed = remove_respecting_locks(&mut deque, &mut locked, 10);
assert_eq!(removed, None);
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4])); // Deque unchanged
assert!(locked.contains(&2)); // Lock unchanged
}
// Test case 7: Remove enough elements to make a locked index invalid
{
let mut deque = VecDeque::from(vec![0, 1, 2]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
remove_respecting_locks(&mut deque, &mut locked, 0);
assert_eq!(deque, VecDeque::from(vec![1, 2]));
assert!(!locked.contains(&2)); // Index 2 should now be invalid
}
// Test case 8: Removing an element before multiple locked indices
{
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4, 5]);
let mut locked = BTreeSet::new();
locked.insert(2); // Lock index 2
locked.insert(4); // Lock index 4
let removed = remove_respecting_locks(&mut deque, &mut locked, 1);
assert_eq!(removed, Some(1));
assert_eq!(deque, VecDeque::from(vec![0, 3, 2, 5, 4]));
assert!(locked.contains(&2) && locked.contains(&4)); // Both indices should still be locked
}
}
}

View File

@@ -10,36 +10,31 @@
use std::env::temp_dir;
use std::net::Shutdown;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::sync::Arc;
#[cfg(feature = "deadlock_detection")]
use std::time::Duration;
use clap::Parser;
use clap::ValueEnum;
use color_eyre::eyre;
use color_eyre::eyre::bail;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_utils::Backoff;
use komorebi::animation::AnimationEngine;
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
use komorebi::animation::AnimationEngine;
use komorebi::replace_env_in_path;
use parking_lot::Mutex;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
use serde::Deserialize;
use sysinfo::Process;
use sysinfo::ProcessesToUpdate;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::EnvFilter;
use uds_windows::UnixStream;
use komorebi::CUSTOM_FFM;
use komorebi::DATA_DIR;
use komorebi::HOME_DIR;
use komorebi::INITIAL_CONFIGURATION_LOADED;
use komorebi::SESSION_ID;
use komorebi::border_manager;
use komorebi::focus_manager;
use komorebi::load_configuration;
@@ -50,36 +45,37 @@ use komorebi::process_event::listen_for_events;
use komorebi::process_movement::listen_for_movements;
use komorebi::reaper;
use komorebi::stackbar_manager;
use komorebi::state::State;
use komorebi::static_config::StaticConfig;
use komorebi::theme_manager;
use komorebi::transparency_manager;
use komorebi::window_manager::State;
use komorebi::window_manager::WindowManager;
use komorebi::windows_api::WindowsApi;
use komorebi::winevent_listener;
use komorebi::CUSTOM_FFM;
use komorebi::DATA_DIR;
use komorebi::HOME_DIR;
use komorebi::INITIAL_CONFIGURATION_LOADED;
use komorebi::SESSION_ID;
fn setup(log_level: LogLevel) -> eyre::Result<(WorkerGuard, WorkerGuard)> {
fn setup(log_level: LogLevel) -> Result<(WorkerGuard, WorkerGuard)> {
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
unsafe {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
color_eyre::install()?;
if std::env::var("RUST_LOG").is_err() {
unsafe {
std::env::set_var(
"RUST_LOG",
match log_level {
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
},
);
}
std::env::set_var(
"RUST_LOG",
match log_level {
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
},
);
}
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
@@ -138,22 +134,20 @@ fn setup(log_level: LogLevel) -> eyre::Result<(WorkerGuard, WorkerGuard)> {
#[tracing::instrument]
fn detect_deadlocks() {
// Create a background thread which checks for deadlocks every 10s
std::thread::spawn(move || {
loop {
tracing::info!("running deadlock detector");
std::thread::sleep(Duration::from_secs(5));
let deadlocks = deadlock::check_deadlock();
if deadlocks.is_empty() {
continue;
}
std::thread::spawn(move || loop {
tracing::info!("running deadlock detector");
std::thread::sleep(Duration::from_secs(5));
let deadlocks = deadlock::check_deadlock();
if deadlocks.is_empty() {
continue;
}
tracing::error!("{} deadlocks detected", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
tracing::error!("deadlock #{}", i);
for t in threads {
tracing::error!("thread id: {:#?}", t.thread_id());
tracing::error!("{:#?}", t.backtrace());
}
tracing::error!("{} deadlocks detected", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
tracing::error!("deadlock #{}", i);
for t in threads {
tracing::error!("thread id: {:#?}", t.thread_id());
tracing::error!("{:#?}", t.backtrace());
}
}
});
@@ -176,7 +170,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
@@ -196,7 +190,7 @@ struct Opts {
#[tracing::instrument]
#[allow(clippy::cognitive_complexity)]
fn main() -> eyre::Result<()> {
fn main() -> Result<()> {
let opts: Opts = Opts::parse();
CUSTOM_FFM.store(opts.focus_follows_mouse, Ordering::SeqCst);
@@ -216,7 +210,9 @@ fn main() -> eyre::Result<()> {
}
if set_foreground_window_retries == 0 {
bail!("failed call to AllowSetForegroundWindow after 5 retries");
return Err(anyhow!(
"failed call to AllowSetForegroundWindow after 5 retries"
));
}
}
@@ -233,17 +229,15 @@ fn main() -> eyre::Result<()> {
if matched_procs.len() > 1 {
let mut len = matched_procs.len();
for proc in matched_procs {
if let Some(executable_path) = proc.exe()
&& executable_path.to_string_lossy().contains("shims")
{
len -= 1;
if let Some(executable_path) = proc.exe() {
if executable_path.to_string_lossy().contains("shims") {
len -= 1;
}
}
}
if len > 1 {
tracing::error!(
"komorebi.exe is already running, please exit the existing process before starting a new one"
);
tracing::error!("komorebi.exe is already running, please exit the existing process before starting a new one");
std::process::exit(1);
}
}

View File

@@ -2,9 +2,13 @@ use std::collections::HashMap;
use std::collections::VecDeque;
use std::sync::atomic::Ordering;
use color_eyre::eyre;
use color_eyre::eyre::OptionExt;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use getset::CopyGetters;
use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use serde::Deserialize;
use serde::Serialize;
@@ -13,40 +17,58 @@ use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::core::Rect;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
use crate::workspace::WorkspaceGlobals;
use crate::workspace::WorkspaceLayer;
use crate::DefaultLayout;
use crate::FloatingLayerBehaviour;
use crate::Layout;
use crate::OperationDirection;
use crate::Wallpaper;
use crate::WindowsApi;
use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
use crate::workspace::WorkspaceGlobals;
use crate::workspace::WorkspaceLayer;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, PartialEq,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Monitor {
#[getset(get_copy = "pub", set = "pub")]
pub id: isize,
#[getset(get = "pub", set = "pub")]
pub name: String,
#[getset(get = "pub", set = "pub")]
pub device: String,
#[getset(get = "pub", set = "pub")]
pub device_id: String,
#[getset(get = "pub", set = "pub")]
pub serial_number_id: Option<String>,
#[getset(get = "pub", set = "pub")]
pub size: Rect,
#[getset(get = "pub", set = "pub")]
pub work_area_size: Rect,
#[getset(get_copy = "pub", set = "pub")]
pub work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
pub window_based_work_area_offset: Option<Rect>,
#[getset(get_copy = "pub", set = "pub")]
pub window_based_work_area_offset_limit: isize,
pub workspaces: Ring<Workspace>,
#[serde(skip_serializing_if = "Option::is_none")]
#[getset(get_copy = "pub", set = "pub")]
pub last_focused_workspace: Option<usize>,
#[getset(get_mut = "pub")]
pub workspace_names: HashMap<usize, String>,
#[getset(get_copy = "pub", set = "pub")]
pub container_padding: Option<i32>,
#[getset(get_copy = "pub", set = "pub")]
pub workspace_padding: Option<i32>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub wallpaper: Option<Wallpaper>,
#[getset(get_copy = "pub", set = "pub")]
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
}
@@ -153,23 +175,23 @@ impl Monitor {
pub fn focused_workspace_name(&self) -> Option<String> {
self.focused_workspace()
.map(|w| w.name.clone())
.map(|w| w.name().clone())
.unwrap_or(None)
}
pub fn focused_workspace_layout(&self) -> Option<Layout> {
self.focused_workspace().and_then(|workspace| {
if workspace.tile {
Some(workspace.layout.clone())
if *workspace.tile() {
Some(workspace.layout().clone())
} else {
None
}
})
}
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> eyre::Result<()> {
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
let focused_idx = self.focused_workspace_idx();
let hmonitor = self.id;
let hmonitor = self.id();
let monitor_wp = self.wallpaper.clone();
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
if i == focused_idx {
@@ -185,10 +207,10 @@ impl Monitor {
/// Updates the `globals` field of all workspaces
pub fn update_workspaces_globals(&mut self, offset: Option<Rect>) {
let container_padding = self
.container_padding
.container_padding()
.or(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
let workspace_padding = self
.workspace_padding
.workspace_padding()
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
let (border_width, border_offset) = {
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
@@ -200,11 +222,11 @@ impl Monitor {
(0, 0)
}
};
let work_area = self.work_area_size;
let work_area = *self.work_area_size();
let work_area_offset = self.work_area_offset.or(offset);
let window_based_work_area_offset = self.window_based_work_area_offset;
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit;
let floating_layer_behaviour = self.floating_layer_behaviour;
let window_based_work_area_offset = self.window_based_work_area_offset();
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit();
let floating_layer_behaviour = self.floating_layer_behaviour();
for workspace in self.workspaces_mut() {
workspace.globals = WorkspaceGlobals {
@@ -224,10 +246,10 @@ impl Monitor {
/// Updates the `globals` field of workspace with index `workspace_idx`
pub fn update_workspace_globals(&mut self, workspace_idx: usize, offset: Option<Rect>) {
let container_padding = self
.container_padding
.container_padding()
.or(Some(DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)));
let workspace_padding = self
.workspace_padding
.workspace_padding()
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
let (border_width, border_offset) = {
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
@@ -239,11 +261,11 @@ impl Monitor {
(0, 0)
}
};
let work_area = self.work_area_size;
let work_area = *self.work_area_size();
let work_area_offset = self.work_area_offset.or(offset);
let window_based_work_area_offset = self.window_based_work_area_offset;
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit;
let floating_layer_behaviour = self.floating_layer_behaviour;
let window_based_work_area_offset = self.window_based_work_area_offset();
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit();
let floating_layer_behaviour = self.floating_layer_behaviour();
if let Some(workspace) = self.workspaces_mut().get_mut(workspace_idx) {
workspace.globals = WorkspaceGlobals {
@@ -264,14 +286,14 @@ impl Monitor {
&mut self,
container: Container,
workspace_idx: Option<usize>,
) -> eyre::Result<()> {
) -> Result<()> {
let workspace = if let Some(idx) = workspace_idx {
self.workspaces_mut()
.get_mut(idx)
.ok_or_eyre(format!("there is no workspace at index {idx}"))?
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
} else {
self.focused_workspace_mut()
.ok_or_eyre("there is no workspace")?
.ok_or_else(|| anyhow!("there is no workspace"))?
};
workspace.add_container_to_back(container);
@@ -288,21 +310,21 @@ impl Monitor {
container: Container,
workspace_idx: Option<usize>,
direction: OperationDirection,
) -> eyre::Result<()> {
) -> Result<()> {
let workspace = if let Some(idx) = workspace_idx {
self.workspaces_mut()
.get_mut(idx)
.ok_or_eyre(format!("there is no workspace at index {idx}"))?
.ok_or_else(|| anyhow!("there is no workspace at index {}", idx))?
} else {
self.focused_workspace_mut()
.ok_or_eyre("there is no workspace")?
.ok_or_else(|| anyhow!("there is no workspace"))?
};
match direction {
OperationDirection::Left => {
// insert the container into the workspace on the monitor at the back (or rightmost position)
// if we are moving across a boundary to the left (back = right side of the target)
match workspace.layout {
match workspace.layout() {
Layout::Default(layout) => match layout {
DefaultLayout::RightMainVerticalStack => {
workspace.add_container_to_front(container);
@@ -326,7 +348,7 @@ impl Monitor {
OperationDirection::Right => {
// insert the container into the workspace on the monitor at the front (or leftmost position)
// if we are moving across a boundary to the right (front = left side of the target)
match workspace.layout {
match workspace.layout() {
Layout::Default(layout) => {
let target_index = layout.leftmost_index(workspace.containers().len());
@@ -390,12 +412,12 @@ impl Monitor {
target_workspace_idx: usize,
follow: bool,
direction: Option<OperationDirection>,
) -> eyre::Result<()> {
) -> Result<()> {
let workspace = self
.focused_workspace_mut()
.ok_or_eyre("there is no workspace")?;
.ok_or_else(|| anyhow!("there is no workspace"))?;
if workspace.maximized_window.is_some() {
if workspace.maximized_window().is_some() {
bail!("cannot move native maximized window to another monitor or workspace");
}
@@ -418,12 +440,12 @@ impl Monitor {
};
target_workspace.floating_windows_mut().push_back(window);
target_workspace.layer = WorkspaceLayer::Floating;
target_workspace.set_layer(WorkspaceLayer::Floating);
}
} else {
let container = workspace
.remove_focused_container()
.ok_or_eyre("there is no container")?;
.ok_or_else(|| anyhow!("there is no container"))?;
let workspaces = self.workspaces_mut();
@@ -436,7 +458,7 @@ impl Monitor {
Some(workspace) => workspace,
};
if target_workspace.monocle_container.is_some() {
if target_workspace.monocle_container().is_some() {
for container in target_workspace.containers_mut() {
container.restore();
}
@@ -448,7 +470,7 @@ impl Monitor {
target_workspace.reintegrate_monocle_container()?;
}
target_workspace.layer = WorkspaceLayer::Tiling;
target_workspace.set_layer(WorkspaceLayer::Tiling);
if let Some(direction) = direction {
self.add_container_with_direction(
@@ -469,7 +491,7 @@ impl Monitor {
}
#[tracing::instrument(skip(self))]
pub fn focus_workspace(&mut self, idx: usize) -> eyre::Result<()> {
pub fn focus_workspace(&mut self, idx: usize) -> Result<()> {
tracing::info!("focusing workspace");
{
@@ -478,7 +500,7 @@ impl Monitor {
if workspaces.get(idx).is_none() {
workspaces.resize(idx + 1, Workspace::default());
}
self.last_focused_workspace = Some(self.workspaces.focused_idx());
self.workspaces.focus(idx);
}
@@ -488,8 +510,8 @@ impl Monitor {
if name.is_some() {
self.workspaces_mut()
.get_mut(idx)
.ok_or_eyre("there is no workspace")?
.name = name;
.ok_or_else(|| anyhow!("there is no workspace"))?
.set_name(name);
}
}
@@ -500,9 +522,9 @@ impl Monitor {
self.workspaces().len()
}
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> eyre::Result<()> {
let offset = if self.work_area_offset.is_some() {
self.work_area_offset
pub fn update_focused_workspace(&mut self, offset: Option<Rect>) -> Result<()> {
let offset = if self.work_area_offset().is_some() {
self.work_area_offset()
} else {
offset
};
@@ -510,7 +532,7 @@ impl Monitor {
let focused_workspace_idx = self.focused_workspace_idx();
self.update_workspace_globals(focused_workspace_idx, offset);
self.focused_workspace_mut()
.ok_or_eyre("there is no workspace")?
.ok_or_else(|| anyhow!("there is no workspace"))?
.update()?;
Ok(())

View File

@@ -1,6 +1,7 @@
use std::sync::mpsc;
use std::time::Duration;
use windows::core::PCWSTR;
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_DISPLAY_ADAPTER;
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_MONITOR;
use windows::Win32::Devices::Display::GUID_DEVINTERFACE_VIDEO_OUTPUT_ARRIVAL;
@@ -10,6 +11,10 @@ use windows::Win32::Foundation::LRESULT;
use windows::Win32::Foundation::WPARAM;
use windows::Win32::System::Power::POWERBROADCAST_SETTING;
use windows::Win32::System::SystemServices::GUID_LIDSWITCH_STATE_CHANGE;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
use windows::Win32::UI::WindowsAndMessaging::DBT_CONFIGCHANGED;
@@ -18,9 +23,6 @@ use windows::Win32::UI::WindowsAndMessaging::DBT_DEVICEREMOVECOMPLETE;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVNODES_CHANGED;
use windows::Win32::UI::WindowsAndMessaging::DBT_DEVTYP_DEVICEINTERFACE;
use windows::Win32::UI::WindowsAndMessaging::DEV_BROADCAST_DEVICEINTERFACE_W;
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMEAUTOMATIC;
use windows::Win32::UI::WindowsAndMessaging::PBT_APMRESUMESUSPEND;
@@ -28,7 +30,6 @@ use windows::Win32::UI::WindowsAndMessaging::PBT_APMSUSPEND;
use windows::Win32::UI::WindowsAndMessaging::PBT_POWERSETTINGCHANGE;
use windows::Win32::UI::WindowsAndMessaging::REGISTER_NOTIFICATION_FLAGS;
use windows::Win32::UI::WindowsAndMessaging::SPI_SETWORKAREA;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::WM_DEVICECHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_DISPLAYCHANGE;
use windows::Win32::UI::WindowsAndMessaging::WM_POWERBROADCAST;
@@ -37,11 +38,10 @@ use windows::Win32::UI::WindowsAndMessaging::WM_WTSSESSION_CHANGE;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_LOCK;
use windows::Win32::UI::WindowsAndMessaging::WTS_SESSION_UNLOCK;
use windows::core::PCWSTR;
use crate::WindowsApi;
use crate::monitor_reconciliator;
use crate::windows_api;
use crate::WindowsApi;
// This is a hidden window specifically spawned to listen to system-wide events related to monitors
#[derive(Debug, Clone, Copy)]
@@ -224,18 +224,14 @@ impl Hidden {
WM_WTSSESSION_CHANGE => {
match wparam.0 as u32 {
WTS_SESSION_LOCK => {
tracing::debug!(
"WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked"
);
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_LOCK - screen locked");
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::SessionLocked,
);
}
WTS_SESSION_UNLOCK => {
tracing::debug!(
"WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked"
);
tracing::debug!("WM_WTSSESSION_CHANGE event received with WTS_SESSION_UNLOCK - screen unlocked");
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::SessionUnlocked,
@@ -255,8 +251,7 @@ impl Hidden {
// and resolution changes here
WM_DISPLAYCHANGE => {
tracing::debug!(
"WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed",
wparam.0
"WM_DISPLAYCHANGE event received with wparam: {}- work area or display resolution changed", wparam.0
);
monitor_reconciliator::send_notification(
@@ -270,8 +265,8 @@ impl Hidden {
#[allow(clippy::cast_possible_truncation)]
if wparam.0 as u32 == SPI_SETWORKAREA.0 {
tracing::debug!(
"WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)"
);
"WM_SETTINGCHANGE event received with SPI_SETWORKAREA - work area changed (probably butterytaskbar or something similar)"
);
monitor_reconciliator::send_notification(
monitor_reconciliator::MonitorNotification::WorkAreaChanged,

View File

@@ -1,12 +1,5 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
use crate::Notification;
use crate::NotificationEvent;
use crate::WORKSPACE_MATCHING_RULES;
use crate::WindowManager;
use crate::WindowsApi;
use crate::border_manager;
use crate::config_generation::WorkspaceMatchingRule;
use crate::core::Rect;
@@ -14,7 +7,14 @@ use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator::hidden::Hidden;
use crate::notify_subscribers;
use crate::state::State;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::WindowManager;
use crate::WindowsApi;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
use crate::WORKSPACE_MATCHING_RULES;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
@@ -22,10 +22,10 @@ use parking_lot::Mutex;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
pub mod hidden;
@@ -70,10 +70,10 @@ pub fn send_notification(notification: MonitorNotification) {
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();
let preferred_id = if dip_ids.any(|id| id.eq(&monitor.device_id)) {
monitor.device_id.clone()
} else if dip_ids.any(|id| Some(id) == monitor.serial_number_id.as_ref()) {
monitor.serial_number_id.clone().unwrap_or_default()
let preferred_id = if dip_ids.any(|id| id == monitor.device_id()) {
monitor.device_id().clone()
} else if dip_ids.any(|id| Some(id) == monitor.serial_number_id().as_ref()) {
monitor.serial_number_id().clone().unwrap_or_default()
} else {
serial_or_device_id.to_string()
};
@@ -100,12 +100,12 @@ where
}
for d in &all_displays {
if let Some(id) = &d.serial_number_id
&& serial_id_map.get(id).copied().unwrap_or_default() > 1
{
let mut dupes = DUPLICATE_MONITOR_SERIAL_IDS.write();
if !dupes.contains(id) {
(*dupes).push(id.clone());
if let Some(id) = &d.serial_number_id {
if serial_id_map.get(id).copied().unwrap_or_default() > 1 {
let mut dupes = DUPLICATE_MONITOR_SERIAL_IDS.write();
if !dupes.contains(id) {
(*dupes).push(id.clone());
}
}
}
}
@@ -155,18 +155,16 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Re
tracing::info!("created hidden window to listen for monitor-related events");
std::thread::spawn(move || {
loop {
match handle_notifications(wm.clone(), win32_display_data::connected_displays_all) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
std::thread::spawn(move || loop {
match handle_notifications(wm.clone(), win32_display_data::connected_displays_all) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
}
}
@@ -221,27 +219,27 @@ where
let mut should_update = false;
// Update work areas as necessary
if let Ok(reference) = WindowsApi::monitor(monitor.id)
&& reference.work_area_size != monitor.work_area_size
{
monitor.work_area_size = Rect {
left: reference.work_area_size.left,
top: reference.work_area_size.top,
right: reference.work_area_size.right,
bottom: reference.work_area_size.bottom,
};
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
if reference.work_area_size() != monitor.work_area_size() {
monitor.set_work_area_size(Rect {
left: reference.work_area_size().left,
top: reference.work_area_size().top,
right: reference.work_area_size().right,
bottom: reference.work_area_size().bottom,
});
should_update = true;
should_update = true;
}
}
if should_update {
tracing::info!("updated work area for {}", monitor.device_id);
tracing::info!("updated work area for {}", monitor.device_id());
monitor.update_focused_workspace(offset)?;
border_manager::send_notification(None);
} else {
tracing::debug!(
"work areas match, reconciliation not required for {}",
monitor.device_id
monitor.device_id()
);
}
}
@@ -253,25 +251,25 @@ where
let mut should_update = false;
// Update sizes and work areas as necessary
if let Ok(reference) = WindowsApi::monitor(monitor.id) {
if reference.work_area_size != monitor.work_area_size {
monitor.work_area_size = Rect {
left: reference.work_area_size.left,
top: reference.work_area_size.top,
right: reference.work_area_size.right,
bottom: reference.work_area_size.bottom,
};
if let Ok(reference) = WindowsApi::monitor(monitor.id()) {
if reference.work_area_size() != monitor.work_area_size() {
monitor.set_work_area_size(Rect {
left: reference.work_area_size().left,
top: reference.work_area_size().top,
right: reference.work_area_size().right,
bottom: reference.work_area_size().bottom,
});
should_update = true;
}
if reference.size != monitor.size {
monitor.size = Rect {
left: reference.size.left,
top: reference.size.top,
right: reference.size.right,
bottom: reference.size.bottom,
};
if reference.size() != monitor.size() {
monitor.set_size(Rect {
left: reference.size().left,
top: reference.size().top,
right: reference.size().right,
bottom: reference.size().bottom,
});
should_update = true;
}
@@ -280,7 +278,7 @@ where
if should_update {
tracing::info!(
"updated monitor resolution/scaling for {}",
monitor.device_id
monitor.device_id()
);
monitor.update_focused_workspace(offset)?;
@@ -288,7 +286,7 @@ where
} else {
tracing::debug!(
"resolutions match, reconciliation not required for {}",
monitor.device_id
monitor.device_id()
);
}
}
@@ -313,21 +311,21 @@ where
for monitor in wm.monitors_mut() {
for attached in &attached_devices {
let serial_number_ids_match = if let (Some(attached_snid), Some(m_snid)) =
(&attached.serial_number_id, &monitor.serial_number_id)
(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;
if serial_number_ids_match || attached.device_id().eq(monitor.device_id()) {
monitor.set_id(attached.id());
monitor.set_device(attached.device().clone());
monitor.set_device_id(attached.device_id().clone());
monitor.set_serial_number_id(attached.serial_number_id().clone());
monitor.set_name(attached.name().clone());
monitor.set_size(*attached.size());
monitor.set_work_area_size(*attached.work_area_size());
}
}
}
@@ -361,13 +359,13 @@ where
for (m_idx, m) in wm.monitors().iter().enumerate() {
if !attached_devices.iter().any(|attached| {
attached.serial_number_id.eq(&m.serial_number_id)
|| attached.device_id.eq(&m.device_id)
attached.serial_number_id().eq(m.serial_number_id())
|| attached.device_id().eq(m.device_id())
}) {
let id = m
.serial_number_id
.serial_number_id()
.as_ref()
.map_or(m.device_id.clone(), |sn| sn.clone());
.map_or(m.device_id().clone(), |sn| sn.clone());
newly_removed_displays.push(id.clone());
@@ -394,7 +392,7 @@ where
}
}
if let Some(maximized) = &workspace.maximized_window {
if let Some(maximized) = workspace.maximized_window() {
windows_to_remove.push(maximized.hwnd);
// Minimize the focused window since Windows might try
// to move it to another monitor if it was focused.
@@ -403,7 +401,7 @@ where
}
}
if let Some(container) = &workspace.monocle_container {
if let Some(container) = workspace.monocle_container() {
for window in container.windows() {
windows_to_remove.push(window.hwnd);
}
@@ -442,10 +440,10 @@ where
// the user set as preference as the id.
let dip = DISPLAY_INDEX_PREFERENCES.read();
let mut dip_ids = dip.values();
let preferred_id = if dip_ids.any(|id| id.eq(&m.device_id)) {
m.device_id.clone()
} else if dip_ids.any(|id| Some(id) == m.serial_number_id.as_ref()) {
m.serial_number_id.clone().unwrap_or_default()
let preferred_id = if dip_ids.any(|id| id == m.device_id()) {
m.device_id().clone()
} else if dip_ids.any(|id| Some(id) == m.serial_number_id().as_ref()) {
m.serial_number_id().clone().unwrap_or_default()
} else {
id
};
@@ -460,8 +458,8 @@ where
// After we have cached them, remove them from our state
wm.monitors_mut().retain(|m| {
!newly_removed_displays.iter().any(|id| {
m.serial_number_id.as_ref().is_some_and(|sn| sn == id)
|| m.device_id.eq(id)
m.serial_number_id().as_ref().is_some_and(|sn| sn == id)
|| m.device_id() == id
})
});
}
@@ -492,7 +490,7 @@ where
let post_removal_device_ids = wm
.monitors()
.iter()
.map(|m| &m.device_id)
.map(Monitor::device_id)
.cloned()
.collect::<Vec<_>>();
@@ -515,23 +513,21 @@ where
// Look in the updated state for new monitors
for (i, m) in wm.monitors_mut().iter_mut().enumerate() {
let device_id = &m.device_id;
let device_id = m.device_id();
// We identify a new monitor when we encounter a new device id
if !post_removal_device_ids.contains(device_id) {
let mut cache_hit = false;
let mut cached_id = String::new();
// Check if that device id exists in the cache for this session
if let Some((id, cached)) = monitor_cache.get_key_value(device_id).or(m
.serial_number_id
.serial_number_id()
.as_ref()
.and_then(|sn| monitor_cache.get_key_value(sn)))
{
cache_hit = true;
cached_id = id.clone();
tracing::info!(
"found monitor and workspace configuration for {id} in the monitor cache, applying"
);
tracing::info!("found monitor and workspace configuration for {id} in the monitor cache, applying");
// If it does, update the cached monitor info with the new one and
// load the cached monitor removing any window that has since been
@@ -612,24 +608,24 @@ where
}
}
if let Some(window) = &workspace.maximized_window {
if let Some(window) = workspace.maximized_window() {
if window.exe().is_err()
|| known_hwnds.contains_key(&window.hwnd)
{
workspace.maximized_window = None;
workspace.set_maximized_window(None);
} else if is_focused_workspace {
WindowsApi::restore_window(window.hwnd);
}
}
if let Some(container) = &mut workspace.monocle_container {
if let Some(container) = workspace.monocle_container_mut() {
container.windows_mut().retain(|window| {
window.exe().is_ok()
&& !known_hwnds.contains_key(&window.hwnd)
});
if container.windows().is_empty() {
workspace.monocle_container = None;
workspace.set_monocle_container(None);
} else if is_focused_workspace {
if let Some(window) = container.focused_window() {
WindowsApi::restore_window(window.hwnd);
@@ -661,7 +657,7 @@ where
let mut workspace_matching_rules =
WORKSPACE_MATCHING_RULES.lock();
if let Some(rules) = workspace
.workspace_config
.workspace_config()
.as_ref()
.and_then(|c| c.workspace_rules.as_ref())
{
@@ -676,7 +672,7 @@ where
}
if let Some(rules) = workspace
.workspace_config
.workspace_config()
.as_ref()
.and_then(|c| c.initial_workspace_rules.as_ref())
{
@@ -738,8 +734,8 @@ where
mod tests {
use super::*;
use crate::window_manager_event::WindowManagerEvent;
use crossbeam_channel::Sender;
use crossbeam_channel::bounded;
use crossbeam_channel::Sender;
use std::path::PathBuf;
use uuid::Uuid;
use windows::Win32::Devices::Display::DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY;
@@ -805,7 +801,7 @@ mod tests {
let wm = match WindowManager::new(receiver, Some(socket_path.clone())) {
Ok(manager) => manager,
Err(e) => {
panic!("Failed to create WindowManager: {e}");
panic!("Failed to create WindowManager: {}", e);
}
};
@@ -833,7 +829,7 @@ mod tests {
Ok(notification) => {
assert_eq!(notification, MonitorNotification::ResolutionScalingChanged);
}
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
}
}
@@ -853,7 +849,7 @@ mod tests {
for _ in 0..20 {
let notification = match receiver.try_recv() {
Ok(notification) => notification,
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
};
assert_eq!(
notification,
@@ -964,7 +960,7 @@ mod tests {
Ok(notification) => {
assert_eq!(notification, MonitorNotification::DisplayConnectionChange);
}
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
}
}
@@ -1010,18 +1006,18 @@ mod tests {
assert_eq!(monitors.len(), 1, "Expected one monitor");
// hmonitor
assert_eq!(monitors[0].id, 1);
assert_eq!(monitors[0].id(), 1);
// device name
assert_eq!(monitors[0].name, String::from("DISPLAY1"));
assert_eq!(monitors[0].name(), &String::from("DISPLAY1"));
// Device
assert_eq!(monitors[0].device, String::from("ABC123"));
assert_eq!(monitors[0].device(), &String::from("ABC123"));
// Device ID
assert_eq!(
monitors[0].device_id,
String::from("ABC123-4&123456&0&UID0")
monitors[0].device_id(),
&String::from("ABC123-4&123456&0&UID0")
);
// Check monitor serial number id

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,8 @@
use std::process::Command;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use color_eyre::eyre;
use color_eyre::eyre::OptionExt;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use crossbeam_utils::atomic::AtomicConsume;
use parking_lot::Mutex;
@@ -12,59 +11,37 @@ use crate::core::Rect;
use crate::core::Sizing;
use crate::core::WindowContainerBehaviour;
use crate::CURRENT_VIRTUAL_DESKTOP;
use crate::DefaultLayout;
use crate::FLOATING_APPLICATIONS;
use crate::HIDDEN_HWNDS;
use crate::Layout;
use crate::Notification;
use crate::NotificationEvent;
use crate::REGEX_IDENTIFIERS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::VirtualDesktopNotification;
use crate::Window;
use crate::border_manager;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::current_virtual_desktop;
use crate::notify_subscribers;
use crate::splash;
use crate::splash::mdm_enrollment;
use crate::stackbar_manager;
use crate::state::State;
use crate::transparency_manager;
use crate::window::RuleDebug;
use crate::window::should_act;
use crate::window::RuleDebug;
use crate::window_manager::WindowManager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::workspace::WorkspaceLayer;
use crate::DefaultLayout;
use crate::Layout;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::VirtualDesktopNotification;
use crate::Window;
use crate::CURRENT_VIRTUAL_DESKTOP;
use crate::FLOATING_APPLICATIONS;
use crate::HIDDEN_HWNDS;
use crate::REGEX_IDENTIFIERS;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
#[tracing::instrument]
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
let receiver = wm.lock().incoming_events.clone();
std::thread::spawn(|| {
loop {
if let Ok((mdm, server)) = mdm_enrollment() {
#[allow(clippy::collapsible_if)]
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);
}
}
let _ = Command::new("komorebic").args(&args).spawn();
}
}
std::thread::sleep(std::time::Duration::from_secs(14400));
}
});
std::thread::spawn(move || {
tracing::info!("listening");
loop {
@@ -88,7 +65,7 @@ pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
impl WindowManager {
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
#[tracing::instrument(skip(self, event), fields(event = event.title(), winevent = event.winevent(), hwnd = event.hwnd()))]
pub fn process_event(&mut self, event: WindowManagerEvent) -> eyre::Result<()> {
pub fn process_event(&mut self, event: WindowManagerEvent) -> Result<()> {
if self.is_paused {
tracing::trace!("ignoring while paused");
return Ok(());
@@ -168,9 +145,7 @@ impl WindowManager {
// to be consumed by integrating gui applications like bars to know
// when to hide visual components which don't make sense when not on
// komorebi's associated virtual desktop
tracing::debug!(
"notifying subscribers that we have left komorebi's associated virtual desktop"
);
tracing::debug!("notifying subscribers that we have left komorebi's associated virtual desktop");
notify_subscribers(
Notification {
event: NotificationEvent::VirtualDesktop(
@@ -190,9 +165,7 @@ impl WindowManager {
// to be consumed by integrating gui applications like bars to know
// when to show visual components associated with komorebi's virtual
// desktop
tracing::debug!(
"notifying subscribers that we are back on komorebi's associated virtual desktop"
);
tracing::debug!("notifying subscribers that we are back on komorebi's associated virtual desktop");
notify_subscribers(
Notification {
event: NotificationEvent::VirtualDesktop(
@@ -230,11 +203,12 @@ impl WindowManager {
//
// This check ensures that we only update the focused monitor when the window
// triggering monitor reconciliation is known to not be tied to a specific monitor.
if let Ok(class) = window.class()
&& class != "OleMainThreadWndClass"
&& self.focused_monitor_idx() != monitor_idx
{
self.focus_monitor(monitor_idx)?;
if let Ok(class) = window.class() {
if class != "OleMainThreadWndClass"
&& self.focused_monitor_idx() != monitor_idx
{
self.focus_monitor(monitor_idx)?;
}
}
}
}
@@ -330,7 +304,7 @@ impl WindowManager {
// containers - this makes floating windows on empty workspaces go into very
// annoying focus change loops which prevents users from interacting with them
if !matches!(
self.focused_workspace()?.layout,
self.focused_workspace()?.layout(),
Layout::Default(DefaultLayout::Scrolling)
) && !self.focused_workspace()?.containers().is_empty()
{
@@ -345,13 +319,13 @@ impl WindowManager {
match floating_window_idx {
None => {
if let Some(w) = &workspace.maximized_window
&& w.hwnd == window.hwnd
{
return Ok(());
if let Some(w) = workspace.maximized_window() {
if w.hwnd == window.hwnd {
return Ok(());
}
}
if let Some(monocle) = &workspace.monocle_container {
if let Some(monocle) = workspace.monocle_container() {
if let Some(window) = monocle.focused_window() {
window.focus(false)?;
}
@@ -359,10 +333,10 @@ impl WindowManager {
workspace.focus_container_by_window(window.hwnd)?;
}
workspace.layer = WorkspaceLayer::Tiling;
workspace.set_layer(WorkspaceLayer::Tiling);
if matches!(
self.focused_workspace()?.layout,
self.focused_workspace()?.layout(),
Layout::Default(DefaultLayout::Scrolling)
) && !self.focused_workspace()?.containers().is_empty()
{
@@ -371,7 +345,7 @@ impl WindowManager {
}
Some(idx) => {
if let Some(_window) = workspace.floating_windows().get(idx) {
workspace.layer = WorkspaceLayer::Floating;
workspace.set_layer(WorkspaceLayer::Floating);
}
}
}
@@ -415,20 +389,23 @@ impl WindowManager {
}
}
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd)
&& let Some(focused_workspace_idx) = self
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
if let Some(focused_workspace_idx) = self
.monitors()
.get(*m_idx)
.map(|m| m.focused_workspace_idx())
&& *m_idx != self.focused_monitor_idx()
&& *w_idx != focused_workspace_idx
{
tracing::debug!(
"ignoring show event for window already associated with another workspace"
);
{
if *m_idx != self.focused_monitor_idx()
&& *w_idx != focused_workspace_idx
{
tracing::debug!(
"ignoring show event for window already associated with another workspace"
);
window.hide();
proceed = false;
window.hide();
proceed = false;
}
}
}
if proceed {
@@ -438,7 +415,7 @@ impl WindowManager {
);
let workspace = self.focused_workspace_mut()?;
let workspace_contains_window = workspace.contains_window(window.hwnd);
let monocle_container = workspace.monocle_container.clone();
let monocle_container = workspace.monocle_container().clone();
if !workspace_contains_window && needs_reconciliation.is_none() {
let floating_applications = FLOATING_APPLICATIONS.lock();
@@ -481,11 +458,11 @@ impl WindowManager {
let center_spawned_floats =
placement.should_center() && workspace.tile;
workspace.floating_windows_mut().push_back(window);
workspace.layer = WorkspaceLayer::Floating;
workspace.set_layer(WorkspaceLayer::Floating);
if center_spawned_floats {
let mut floating_window = window;
floating_window.center(
&workspace.globals.work_area,
&workspace.globals().work_area,
placement.should_resize(),
)?;
}
@@ -494,15 +471,17 @@ impl WindowManager {
match behaviour.current_behaviour {
WindowContainerBehaviour::Create => {
workspace.new_container_for_window(window);
workspace.layer = WorkspaceLayer::Tiling;
workspace.set_layer(WorkspaceLayer::Tiling);
self.update_focused_workspace(false, false)?;
}
WindowContainerBehaviour::Append => {
workspace
.focused_container_mut()
.ok_or_eyre("there is no focused container")?
.ok_or_else(|| {
anyhow!("there is no focused container")
})?
.add_window(window);
workspace.layer = WorkspaceLayer::Tiling;
workspace.set_layer(WorkspaceLayer::Tiling);
self.update_focused_workspace(true, false)?;
stackbar_manager::send_notification();
}
@@ -527,15 +506,17 @@ impl WindowManager {
if workspace_contains_window {
let mut monocle_window_event = false;
if let Some(ref monocle) = monocle_container
&& let Some(monocle_window) = monocle.focused_window()
&& monocle_window.hwnd == window.hwnd
{
monocle_window_event = true;
if let Some(ref monocle) = monocle_container {
if let Some(monocle_window) = monocle.focused_window() {
if monocle_window.hwnd == window.hwnd {
monocle_window_event = true;
}
}
}
let workspace = self.focused_workspace()?;
if !(monocle_window_event || workspace.layer != WorkspaceLayer::Tiling)
if !(monocle_window_event
|| workspace.layer() != &WorkspaceLayer::Tiling)
&& monocle_container.is_some()
{
window.hide();
@@ -548,7 +529,7 @@ impl WindowManager {
let monitor_idx = self.focused_monitor_idx();
let workspace_idx = self
.focused_monitor()
.ok_or_eyre("there is no monitor with this idx")?
.ok_or_else(|| anyhow!("there is no monitor with this idx"))?
.focused_workspace_idx();
WindowsApi::bring_window_to_top(window.hwnd)?;
@@ -566,19 +547,19 @@ impl WindowManager {
// If the window handles don't match then something went wrong and the pending move
// is not related to this current move, if so abort this operation.
if let Some((_, _, w_hwnd)) = pending
&& w_hwnd != window.hwnd
{
color_eyre::eyre::bail!(
"window handles for move operation don't match: {} != {}",
w_hwnd,
window.hwnd
);
if let Some((_, _, w_hwnd)) = pending {
if w_hwnd != window.hwnd {
color_eyre::eyre::bail!(
"window handles for move operation don't match: {} != {}",
w_hwnd,
window.hwnd
);
}
}
let target_monitor_idx = self
.monitor_idx_from_current_pos()
.ok_or_eyre("cannot get monitor idx from current position")?;
.ok_or_else(|| anyhow!("cannot get monitor idx from current position"))?;
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self.focused_workspace_idx().unwrap_or_default();
@@ -589,7 +570,7 @@ impl WindowManager {
let focused_container_idx = workspace.focused_container_idx();
let new_position = WindowsApi::window_rect(window.hwnd)?;
let old_position = *workspace
.latest_layout
.latest_layout()
.get(focused_container_idx)
// If the move was to another monitor with an empty workspace, the
// workspace here will refer to that empty workspace, which won't
@@ -601,10 +582,10 @@ impl WindowManager {
// This will be true if we have moved to another monitor
let mut moved_across_monitors = false;
if let Some((m_idx, _)) = self.known_hwnds.get(&window.hwnd)
&& *m_idx != target_monitor_idx
{
moved_across_monitors = true;
if let Some((m_idx, _)) = self.known_hwnds.get(&window.hwnd) {
if *m_idx != target_monitor_idx {
moved_across_monitors = true;
}
}
if let Some((origin_monitor_idx, origin_workspace_idx, _)) = pending {
@@ -622,10 +603,10 @@ impl WindowManager {
let origin_workspace = self
.monitors()
.get(origin_monitor_idx)
.ok_or_eyre("cannot get monitor idx")?
.ok_or_else(|| anyhow!("cannot get monitor idx"))?
.workspaces()
.get(origin_workspace_idx)
.ok_or_eyre("cannot get workspace idx")?;
.ok_or_else(|| anyhow!("cannot get workspace idx"))?;
let managed_window = origin_workspace.contains_window(window.hwnd);
@@ -637,7 +618,7 @@ impl WindowManager {
}
let workspace = self.focused_workspace_mut()?;
if (workspace.tile && workspace.contains_managed_window(window.hwnd))
if (*workspace.tile() && workspace.contains_managed_window(window.hwnd))
|| moved_across_monitors
{
let resize = Rect {
@@ -665,15 +646,17 @@ impl WindowManager {
let target_workspace_idx = self
.monitors()
.get(target_monitor_idx)
.ok_or_eyre("there is no monitor at this idx")?
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
.focused_workspace_idx();
let target_container_idx = self
.monitors()
.get(target_monitor_idx)
.ok_or_eyre("there is no monitor at this idx")?
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?
.focused_workspace()
.ok_or_eyre("there is no focused workspace for this monitor")?
.ok_or_else(|| {
anyhow!("there is no focused workspace for this monitor")
})?
.container_idx_from_current_point()
// Default to 0 in the case of an empty workspace
.unwrap_or(0);
@@ -693,7 +676,7 @@ impl WindowManager {
let origin_monitor = self
.monitors_mut()
.get_mut(origin_monitor_idx)
.ok_or_eyre("there is no monitor at this idx")?;
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
origin_monitor.focus_workspace(origin_workspace_idx)?;
self.update_focused_workspace(false, false)?;
@@ -701,7 +684,7 @@ impl WindowManager {
let target_monitor = self
.monitors_mut()
.get_mut(target_monitor_idx)
.ok_or_eyre("there is no monitor at this idx")?;
.ok_or_else(|| anyhow!("there is no monitor at this idx"))?;
target_monitor.focus_workspace(target_workspace_idx)?;
self.update_focused_workspace(false, false)?;
@@ -853,7 +836,7 @@ impl WindowManager {
.and_then(|m| m.workspaces().get(*ws_idx))
{
if let Some(monocle_with_window) = target_workspace
.monocle_container
.monocle_container()
.as_ref()
.and_then(|m| m.contains_window(window.hwnd).then_some(m))
{
@@ -866,12 +849,13 @@ impl WindowManager {
if let Some(target_container) =
c_idx.and_then(|c_idx| target_workspace.containers().get(c_idx))
&& target_container.focused_window() != Some(&window)
{
tracing::debug!(
"Needs reconciliation within a stack on the focused workspace"
);
needs_reconciliation = Some((*m_idx, *ws_idx));
if target_container.focused_window() != Some(&window) {
tracing::debug!(
"Needs reconciliation within a stack on the focused workspace"
);
needs_reconciliation = Some((*m_idx, *ws_idx));
}
}
}
}
@@ -902,13 +886,13 @@ impl WindowManager {
if let Some(monitor) = self.focused_monitor_mut() {
if ws_idx != monitor.focused_workspace_idx() {
let previous_idx = monitor.focused_workspace_idx();
monitor.last_focused_workspace = Option::from(previous_idx);
monitor.set_last_focused_workspace(Option::from(previous_idx));
monitor.focus_workspace(ws_idx)?;
}
if let Some(workspace) = monitor.focused_workspace_mut() {
let mut layer = WorkspaceLayer::Tiling;
if let Some((monocle, idx)) = workspace
.monocle_container
.monocle_container_mut()
.as_mut()
.and_then(|m| m.idx_for_window(window.hwnd).map(|i| (m, i)))
{
@@ -919,14 +903,14 @@ impl WindowManager {
.any(|w| w.hwnd == window.hwnd)
{
layer = WorkspaceLayer::Floating;
} else if workspace
.maximized_window
.is_none_or(|w| w.hwnd != window.hwnd)
} else if !workspace
.maximized_window()
.is_some_and(|w| w.hwnd == window.hwnd)
{
// If the window is the maximized window do nothing, else we
// reintegrate the monocle if it exists and then focus the
// container
if workspace.monocle_container.is_some() {
if workspace.monocle_container().is_some() {
tracing::info!("disabling monocle");
for container in workspace.containers_mut() {
container.restore();
@@ -938,7 +922,7 @@ impl WindowManager {
}
workspace.focus_container_by_window(window.hwnd)?;
}
workspace.layer = layer;
workspace.set_layer(layer);
}
monitor.load_focused_workspace(mouse_follows_focus)?;
monitor.update_focused_workspace(offset)?;

View File

@@ -1,9 +1,9 @@
use std::sync::Arc;
use parking_lot::Mutex;
use winput::Action;
use winput::message_loop;
use winput::message_loop::Event;
use winput::Action;
use crate::core::FocusFollowsMouseImplementation;

View File

@@ -1,15 +1,15 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::DATA_DIR;
use crate::HIDING_BEHAVIOUR;
use crate::border_manager;
use crate::notify_subscribers;
use crate::winevent::WinEvent;
use crate::HidingBehaviour;
use crate::NotificationEvent;
use crate::Window;
use crate::WindowManager;
use crate::WindowManagerEvent;
use crate::border_manager;
use crate::notify_subscribers;
use crate::winevent::WinEvent;
use crate::DATA_DIR;
use crate::HIDING_BEHAVIOUR;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
@@ -55,15 +55,13 @@ pub fn listen_for_notifications(
) {
watch_for_orphans(known_hwnds);
std::thread::spawn(move || {
loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
tracing::warn!("restarting failed thread: {}", error);
}
}
});
@@ -152,18 +150,16 @@ fn watch_for_orphans(known_hwnds: HashMap<isize, (usize, usize)>) {
*cache = known_hwnds;
}
std::thread::spawn(move || {
loop {
match find_orphans() {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
std::thread::spawn(move || loop {
match find_orphans() {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
}
}
@@ -183,7 +179,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

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