mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-15 01:37:42 +01:00
Compare commits
77 Commits
feature/sc
...
v0.1.40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bed314b866 | ||
|
|
c165172b5a | ||
|
|
9977cca500 | ||
|
|
5d7a0ea9ad | ||
|
|
0e79c58be3 | ||
|
|
09205bfd83 | ||
|
|
98122bd9d4 | ||
|
|
9741b387a7 | ||
|
|
1dad13106a | ||
|
|
0b5141e7a4 | ||
|
|
22e8a79833 | ||
|
|
5946caaf92 | ||
|
|
01d73b7d19 | ||
|
|
9d16197825 | ||
|
|
fed09689b8 | ||
|
|
c0b298c9de | ||
|
|
5fd7017b71 | ||
|
|
dbde351e22 | ||
|
|
95838fc896 | ||
|
|
bdef1448e1 | ||
|
|
0b39551470 | ||
|
|
ab528f799d | ||
|
|
8805fafa99 | ||
|
|
1219c3d118 | ||
|
|
b792328676 | ||
|
|
a7380db47c | ||
|
|
086993a7c0 | ||
|
|
51e1337f40 | ||
|
|
6cef8d9ef6 | ||
|
|
6e36b81669 | ||
|
|
0758c7d900 | ||
|
|
f77a303b30 | ||
|
|
3a81f7babb | ||
|
|
3d8778a7d6 | ||
|
|
a42e809ade | ||
|
|
66c5766848 | ||
|
|
ac56590791 | ||
|
|
4d67f5fed3 | ||
|
|
c91224295f | ||
|
|
4b1e3bd448 | ||
|
|
1cb8ed7f10 | ||
|
|
90271f1f99 | ||
|
|
9fde9ed6f8 | ||
|
|
ce4479eb9f | ||
|
|
f3b7f5ac42 | ||
|
|
382896dfd2 | ||
|
|
ff4187aecf | ||
|
|
8f982e45d1 | ||
|
|
309bfdfee1 | ||
|
|
a0239faea0 | ||
|
|
999dd5d20d | ||
|
|
69086299e1 | ||
|
|
4114b10b05 | ||
|
|
71402fe01b | ||
|
|
5c2767d589 | ||
|
|
ac67c41377 | ||
|
|
faed6ec535 | ||
|
|
4ebba08b7d | ||
|
|
a733fecb49 | ||
|
|
31c969fc55 | ||
|
|
df2adde13d | ||
|
|
a77b3e774a | ||
|
|
c8c4c3507c | ||
|
|
df38facf9e | ||
|
|
416dd94670 | ||
|
|
d0ae92ca3a | ||
|
|
3491dc7590 | ||
|
|
adbb6c1cb0 | ||
|
|
18ee667896 | ||
|
|
1a42c64620 | ||
|
|
b613986474 | ||
|
|
e93751aa5f | ||
|
|
9872dcd0d7 | ||
|
|
f99c82f5d4 | ||
|
|
b255058467 | ||
|
|
2abe618354 | ||
|
|
e953715fef |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.json text diff
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -55,6 +55,7 @@ body:
|
||||
label: Hotkey Configuration
|
||||
description: >
|
||||
Please provide your whkdrc or komorebi.ahk hotkey configuration file
|
||||
render: shell
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
@@ -62,3 +63,4 @@ body:
|
||||
label: Output of komorebic check
|
||||
description: >
|
||||
Please provide the output of `komorebic check`
|
||||
render: shell
|
||||
|
||||
22
.github/workflows/windows.yaml
vendored
22
.github/workflows/windows.yaml
vendored
@@ -13,15 +13,15 @@ on:
|
||||
- hotfix/*
|
||||
tags:
|
||||
- v*
|
||||
schedule:
|
||||
- cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
# schedule:
|
||||
# - cron: "30 0 * * 0" # Every day at 00:30 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: rustup toolchain install stable --profile minimal
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
- run: |
|
||||
cargo install cargo-wix
|
||||
cargo wix --no-build -p komorebi --nocapture -I .\wix\main.wxs --target ${{ matrix.platform.target }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: komorebi-${{ matrix.platform.target }}-${{ github.sha }}
|
||||
path: |
|
||||
@@ -82,12 +82,12 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v7
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
@@ -129,14 +129,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v7
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
@@ -171,14 +171,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v7
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -6,3 +6,9 @@ dummy.go
|
||||
komorebic/applications.yaml
|
||||
komorebic/applications.json
|
||||
/.vs
|
||||
/bar-schema
|
||||
/komorebi-schema
|
||||
/.wrangler
|
||||
/.xwin-cache
|
||||
result
|
||||
/.direnv
|
||||
|
||||
2547
Cargo.lock
generated
2547
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
80
Cargo.toml
80
Cargo.toml
@@ -2,14 +2,15 @@
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"komorebi",
|
||||
"komorebi-client",
|
||||
"komorebi-gui",
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes",
|
||||
"komorebi-shortcuts",
|
||||
"komorebi",
|
||||
"komorebi-client",
|
||||
"komorebi-gui",
|
||||
"komorebi-layouts",
|
||||
"komorebic",
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes",
|
||||
"komorebi-shortcuts",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -19,12 +20,12 @@ chrono = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
crossbeam-utils = "0.8"
|
||||
color-eyre = "0.6"
|
||||
eframe = "0.32"
|
||||
egui_extras = "0.32"
|
||||
eframe = "0.33"
|
||||
egui_extras = "0.33"
|
||||
dirs = "6"
|
||||
dunce = "1"
|
||||
hotwatch = "0.5"
|
||||
schemars = "0.8"
|
||||
schemars = "1.1"
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
||||
@@ -35,7 +36,7 @@ tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
parking_lot = "0.12"
|
||||
paste = "1"
|
||||
sysinfo = "0.37"
|
||||
sysinfo = "0.38"
|
||||
uds_windows = "1"
|
||||
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "8c42d8db257d30fe95bc98c2e5cd8f75da861021" }
|
||||
windows-numerics = { version = "0.3" }
|
||||
@@ -48,32 +49,32 @@ which = "8"
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.62"
|
||||
features = [
|
||||
"Foundation_Numerics",
|
||||
"Win32_Devices",
|
||||
"Win32_Devices_Display",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Power",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Accessibility",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control",
|
||||
"Foundation_Numerics",
|
||||
"Win32_Devices",
|
||||
"Win32_Devices_Display",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Power",
|
||||
"Win32_System_RemoteDesktop",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Accessibility",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control",
|
||||
]
|
||||
|
||||
[profile.release-opt]
|
||||
@@ -82,3 +83,6 @@ lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "komorebi-workspace"
|
||||
|
||||
32
README.md
32
README.md
@@ -29,6 +29,36 @@ Tiling Window Management for Windows.
|
||||
|
||||

|
||||
|
||||
## Note: Students using devices enrolled in mobile device management (MDM)
|
||||
|
||||
Your usage still falls under the [Komorebi License 2.0.0](./LICENSE.md).
|
||||
|
||||
You can email me at the address I sign my commits with (add `.patch` to the end
|
||||
of any commit URL on GitHub to find it) from the address associated with your
|
||||
institution with the subject "komorebi - student with an MDM device", and I will
|
||||
be able to remove the splash intended for corporate users, whose usage falls
|
||||
under the [Individual Commercial Use
|
||||
License](https://lgug2z.com/software/komorebi).
|
||||
|
||||
This is currently a manual process - most days this shouldn't take more than
|
||||
12h, and you will receive an email reply from me when the process is complete.
|
||||
|
||||
If you haven't had a reply to your email within 24h you can reach out to me on
|
||||
Discord.
|
||||
|
||||
## Note: Unexpected mobile device management (MDM) detection prompts
|
||||
|
||||
You have most likely unintentionally enrolled your device in "Bring Your Own
|
||||
Device" (BYOD) MDM. You can confirm if this is the case by running `dsregcmd
|
||||
/status` and then take the appropriate steps to remove the MDM profile and take
|
||||
back full control of your system.
|
||||
|
||||
If you need help doing this you can ask on Discord.
|
||||
|
||||
## Note: komorebi for Mac
|
||||
|
||||
komorebi for Mac lives [here](https://github.com/LGUG2Z/komorebi-for-mac) :)
|
||||
|
||||
## Overview
|
||||
|
||||
_komorebi_ is a tiling window manager that works as an extension to Microsoft's
|
||||
@@ -394,7 +424,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.38"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi" }
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
|
||||
432
check_schema_docs.py
Normal file
432
check_schema_docs.py
Normal file
@@ -0,0 +1,432 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check schema.json and schema.bar.json for missing docstrings and map them to Rust source files.
|
||||
|
||||
This script analyzes the generated JSON schemas and identifies:
|
||||
1. Type definitions ($defs) missing top-level descriptions
|
||||
2. Enum variants missing descriptions (in oneOf/anyOf)
|
||||
3. Enum variants missing titles (object variants in oneOf/anyOf)
|
||||
4. Struct properties missing descriptions
|
||||
5. Top-level schema properties missing descriptions
|
||||
|
||||
For each missing docstring, it attempts to find the corresponding Rust source
|
||||
file and line number where the docstring should be added.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class MissingDoc:
|
||||
type_name: str
|
||||
kind: str # "type", "variant", "property", "variant_title"
|
||||
item_name: Optional[str] # variant or property name
|
||||
rust_file: Optional[str] = None
|
||||
rust_line: Optional[int] = None
|
||||
|
||||
def __str__(self):
|
||||
location = ""
|
||||
if self.rust_file and self.rust_line:
|
||||
location = f" -> {self.rust_file}:{self.rust_line}"
|
||||
elif self.rust_file:
|
||||
location = f" -> {self.rust_file}"
|
||||
|
||||
if self.kind == "type":
|
||||
return f"[TYPE] {self.type_name}{location}"
|
||||
elif self.kind == "variant":
|
||||
return f"[VARIANT] {self.type_name}::{self.item_name}{location}"
|
||||
elif self.kind == "variant_title":
|
||||
return f"[VARIANT_TITLE] {self.type_name}::{self.item_name}{location}"
|
||||
else:
|
||||
return f"[PROPERTY] {self.type_name}.{self.item_name}{location}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SchemaConfig:
|
||||
"""Configuration for a schema to check."""
|
||||
|
||||
schema_file: str
|
||||
search_paths: list[str]
|
||||
display_name: str
|
||||
|
||||
|
||||
def find_rust_definition(
|
||||
type_name: str, item_name: Optional[str], kind: str, search_paths: list[Path]
|
||||
) -> tuple[Optional[str], Optional[int]]:
|
||||
"""Find the Rust file and line number for a type/variant/property definition."""
|
||||
|
||||
if kind == "type":
|
||||
patterns = [
|
||||
rf"pub\s+enum\s+{type_name}\b",
|
||||
rf"pub\s+struct\s+{type_name}\b",
|
||||
]
|
||||
elif kind in ("variant", "variant_title"):
|
||||
patterns = [
|
||||
rf"^\s*{re.escape(item_name)}\s*[,\(\{{]",
|
||||
rf"^\s*{re.escape(item_name)}\s*$",
|
||||
rf"^\s*#\[.*\]\s*\n\s*{re.escape(item_name)}\b",
|
||||
]
|
||||
else: # property
|
||||
patterns = [rf"pub\s+{re.escape(item_name)}\s*:"]
|
||||
|
||||
for search_path in search_paths:
|
||||
for rust_file in search_path.rglob("*.rs"):
|
||||
try:
|
||||
content = rust_file.read_text()
|
||||
lines = content.split("\n")
|
||||
|
||||
if kind == "type":
|
||||
for pattern in patterns:
|
||||
for i, line in enumerate(lines):
|
||||
if re.search(pattern, line):
|
||||
return str(rust_file), i + 1
|
||||
|
||||
elif kind in ("variant", "variant_title", "property"):
|
||||
parent_pattern = rf"pub\s+(?:enum|struct)\s+{type_name}\b"
|
||||
in_type = False
|
||||
brace_count = 0
|
||||
found_open_brace = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if re.search(parent_pattern, line):
|
||||
in_type = True
|
||||
brace_count = 0
|
||||
found_open_brace = False
|
||||
|
||||
if in_type:
|
||||
if "{" in line:
|
||||
found_open_brace = True
|
||||
brace_count += line.count("{") - line.count("}")
|
||||
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, line):
|
||||
return str(rust_file), i + 1
|
||||
|
||||
if found_open_brace and brace_count <= 0:
|
||||
in_type = False
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def _get_variant_identifier(variant: dict) -> str:
|
||||
"""Extract a meaningful identifier for a variant.
|
||||
|
||||
Tries to find the best identifier by checking:
|
||||
1. A top-level const value (e.g., {"const": "Linear"})
|
||||
2. A property with a const value (e.g., {"kind": {"const": "Bar"}})
|
||||
3. The first required property name
|
||||
4. The type field
|
||||
5. Falls back to "unknown"
|
||||
"""
|
||||
# Check for top-level const value (simple enum variant)
|
||||
if "const" in variant:
|
||||
return str(variant["const"])
|
||||
|
||||
properties = variant.get("properties", {})
|
||||
|
||||
# Check for a property with a const value (common pattern for tagged enums)
|
||||
for prop_name, prop_def in properties.items():
|
||||
if isinstance(prop_def, dict) and "const" in prop_def:
|
||||
return str(prop_def["const"])
|
||||
|
||||
# Fall back to first required property name
|
||||
required = variant.get("required", [])
|
||||
if required:
|
||||
return str(required[0])
|
||||
|
||||
# Fall back to type
|
||||
if "type" in variant:
|
||||
return str(variant["type"])
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def check_type_description(type_name: str, type_def: dict) -> list[MissingDoc]:
|
||||
"""Check if a type definition has proper documentation."""
|
||||
missing = []
|
||||
has_top_description = "description" in type_def
|
||||
|
||||
# Always check for top-level type description first
|
||||
# (except for types that are purely references or have special handling)
|
||||
needs_type_description = True
|
||||
|
||||
# Check oneOf variants (tagged enums with variant descriptions)
|
||||
if "oneOf" in type_def:
|
||||
# oneOf types should have a top-level description
|
||||
if not has_top_description:
|
||||
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||
|
||||
for variant in type_def["oneOf"]:
|
||||
# Case 1: Simple const variant (e.g., {"const": "Swap", "description": "..."})
|
||||
variant_name = variant.get("const") or variant.get("title")
|
||||
if variant_name and "description" not in variant:
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant", str(variant_name), None, None)
|
||||
)
|
||||
|
||||
# Case 2: String enum inside oneOf (e.g., {"type": "string", "enum": [...]})
|
||||
# These variants don't have individual descriptions in the schema
|
||||
if "enum" in variant and variant.get("type") == "string":
|
||||
for enum_variant in variant["enum"]:
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant", str(enum_variant), None, None)
|
||||
)
|
||||
|
||||
# Case 3: Object variant with properties (e.g., CubicBezier)
|
||||
if "properties" in variant and "description" not in variant:
|
||||
for prop_name in variant.get("required", []):
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant", str(prop_name), None, None)
|
||||
)
|
||||
|
||||
# Case 4: Object variant missing title (needed for schema UI display)
|
||||
# Object variants should have a title or const for proper display in editors
|
||||
if (
|
||||
"properties" in variant
|
||||
and "title" not in variant
|
||||
and "const" not in variant
|
||||
):
|
||||
# Try to find a good identifier for the variant (for display only)
|
||||
variant_id = _get_variant_identifier(variant)
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant_title", str(variant_id), None, None)
|
||||
)
|
||||
|
||||
# Check anyOf variants - check each variant individually
|
||||
elif "anyOf" in type_def:
|
||||
# anyOf types should have a top-level description
|
||||
if not has_top_description:
|
||||
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||
|
||||
# Check each variant for description (skip pure $ref and null types)
|
||||
for variant in type_def["anyOf"]:
|
||||
# Skip null type variants (used for Option<T>)
|
||||
if variant.get("type") == "null":
|
||||
continue
|
||||
# Skip pure $ref variants (the referenced type is checked separately)
|
||||
if "$ref" in variant and len(variant) == 1:
|
||||
continue
|
||||
# Skip $ref variants that have a description
|
||||
if "$ref" in variant and "description" in variant:
|
||||
continue
|
||||
# Variant with $ref but no description
|
||||
if "$ref" in variant and "description" not in variant:
|
||||
# Extract the type name from the $ref
|
||||
ref_name = variant["$ref"].split("/")[-1]
|
||||
missing.append(MissingDoc(type_name, "variant", ref_name, None, None))
|
||||
# Non-ref variant without description
|
||||
elif "description" not in variant and "$ref" not in variant:
|
||||
# Try to identify the variant by its type or const
|
||||
variant_id = variant.get("const") or variant.get("type") or "unknown"
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant", str(variant_id), None, None)
|
||||
)
|
||||
|
||||
# Check for missing title on object variants in anyOf
|
||||
if (
|
||||
"properties" in variant
|
||||
and "title" not in variant
|
||||
and "const" not in variant
|
||||
):
|
||||
variant_id = _get_variant_identifier(variant)
|
||||
missing.append(
|
||||
MissingDoc(type_name, "variant_title", str(variant_id), None, None)
|
||||
)
|
||||
|
||||
# Check simple string enums (no oneOf means no variant descriptions possible in schema)
|
||||
elif "enum" in type_def:
|
||||
if not has_top_description:
|
||||
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||
# Each enum variant needs a docstring - these can't have descriptions in simple enum format
|
||||
for variant in type_def["enum"]:
|
||||
missing.append(MissingDoc(type_name, "variant", str(variant), None, None))
|
||||
|
||||
# Check struct properties
|
||||
elif "properties" in type_def:
|
||||
# Structs should always have a top-level description
|
||||
if not has_top_description:
|
||||
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||
|
||||
for prop_name, prop_def in type_def["properties"].items():
|
||||
if "description" not in prop_def:
|
||||
missing.append(MissingDoc(type_name, "property", prop_name, None, None))
|
||||
|
||||
# Simple type without description (like PathBuf, Hex)
|
||||
elif not has_top_description:
|
||||
# Only flag if it has a concrete type (not just a $ref)
|
||||
if type_def.get("type") is not None:
|
||||
missing.append(MissingDoc(type_name, "type", None, None, None))
|
||||
|
||||
return missing
|
||||
|
||||
|
||||
def check_top_level_properties(schema: dict, root_type_name: str) -> list[MissingDoc]:
|
||||
"""Check top-level schema properties for missing descriptions."""
|
||||
missing = []
|
||||
properties = schema.get("properties", {})
|
||||
|
||||
for prop_name, prop_def in properties.items():
|
||||
if "description" not in prop_def:
|
||||
missing.append(
|
||||
MissingDoc(root_type_name, "property", prop_name, None, None)
|
||||
)
|
||||
|
||||
return missing
|
||||
|
||||
|
||||
def check_schema(
|
||||
schema_path: Path,
|
||||
search_paths: list[Path],
|
||||
project_root: Path,
|
||||
display_name: str,
|
||||
) -> tuple[list[MissingDoc], int]:
|
||||
"""Check a single schema file and return missing docs and exit code."""
|
||||
if not schema_path.exists():
|
||||
print(f"Error: {schema_path.name} not found at {schema_path}")
|
||||
return [], 1
|
||||
|
||||
with open(schema_path) as f:
|
||||
schema = json.load(f)
|
||||
|
||||
all_missing: list[MissingDoc] = []
|
||||
|
||||
# Check top-level schema properties
|
||||
root_type_name = schema.get("title", "Root")
|
||||
all_missing.extend(check_top_level_properties(schema, root_type_name))
|
||||
|
||||
# Check all type definitions
|
||||
for type_name, type_def in sorted(schema.get("$defs", {}).items()):
|
||||
# Skip PerAnimationPrefixConfig2/3 as they're generated variants
|
||||
if (
|
||||
type_name.startswith("PerAnimationPrefixConfig")
|
||||
and type_name != "PerAnimationPrefixConfig"
|
||||
):
|
||||
continue
|
||||
all_missing.extend(check_type_description(type_name, type_def))
|
||||
|
||||
# Find Rust source locations
|
||||
print(f"Scanning Rust source files for {display_name}...", file=sys.stderr)
|
||||
for doc in all_missing:
|
||||
doc.rust_file, doc.rust_line = find_rust_definition(
|
||||
doc.type_name, doc.item_name, doc.kind, search_paths
|
||||
)
|
||||
if doc.rust_file:
|
||||
try:
|
||||
doc.rust_file = str(Path(doc.rust_file).relative_to(project_root))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return all_missing, 0
|
||||
|
||||
|
||||
def print_results(all_missing: list[MissingDoc], display_name: str) -> None:
|
||||
"""Print the results for a schema check."""
|
||||
# Group by file
|
||||
by_file: dict[str, list[MissingDoc]] = {}
|
||||
external: list[MissingDoc] = []
|
||||
|
||||
for doc in all_missing:
|
||||
if doc.rust_file:
|
||||
by_file.setdefault(doc.rust_file, []).append(doc)
|
||||
else:
|
||||
external.append(doc)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 70)
|
||||
print(f"MISSING DOCSTRINGS IN SCHEMA ({display_name})")
|
||||
print("=" * 70)
|
||||
|
||||
type_count = sum(1 for d in all_missing if d.kind == "type")
|
||||
variant_count = sum(1 for d in all_missing if d.kind == "variant")
|
||||
variant_title_count = sum(1 for d in all_missing if d.kind == "variant_title")
|
||||
prop_count = sum(1 for d in all_missing if d.kind == "property")
|
||||
|
||||
print(f"\nTotal: {len(all_missing)} missing docstrings/titles")
|
||||
print(f" - {type_count} types")
|
||||
print(f" - {variant_count} variants")
|
||||
print(f" - {variant_title_count} variant titles")
|
||||
print(f" - {prop_count} properties")
|
||||
|
||||
# Print by file
|
||||
for rust_file in sorted(by_file.keys()):
|
||||
docs = sorted(by_file[rust_file], key=lambda d: d.rust_line or 0)
|
||||
print(f"\n{rust_file}:")
|
||||
print("-" * len(rust_file))
|
||||
for doc in docs:
|
||||
print(f" {doc}")
|
||||
|
||||
# Print external items (types not found in source)
|
||||
if external:
|
||||
print(f"\nExternal/Unknown location:")
|
||||
print("-" * 25)
|
||||
for doc in external:
|
||||
print(f" {doc}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
|
||||
|
||||
def main():
|
||||
project_root = Path.cwd()
|
||||
|
||||
# Define schemas to check with their respective search paths
|
||||
schemas = [
|
||||
SchemaConfig(
|
||||
schema_file="schema.json",
|
||||
search_paths=["komorebi/src", "komorebi-themes/src"],
|
||||
display_name="komorebi",
|
||||
),
|
||||
SchemaConfig(
|
||||
schema_file="schema.bar.json",
|
||||
search_paths=["komorebi-bar/src", "komorebi-themes/src"],
|
||||
display_name="komorebi-bar",
|
||||
),
|
||||
]
|
||||
|
||||
total_missing = 0
|
||||
has_errors = False
|
||||
|
||||
for schema_config in schemas:
|
||||
schema_path = project_root / schema_config.schema_file
|
||||
search_paths = [
|
||||
project_root / p
|
||||
for p in schema_config.search_paths
|
||||
if (project_root / p).exists()
|
||||
]
|
||||
|
||||
missing, error_code = check_schema(
|
||||
schema_path,
|
||||
search_paths,
|
||||
project_root,
|
||||
schema_config.display_name,
|
||||
)
|
||||
|
||||
if error_code != 0:
|
||||
has_errors = True
|
||||
continue
|
||||
|
||||
print_results(missing, schema_config.display_name)
|
||||
total_missing += len(missing)
|
||||
|
||||
# Print combined summary
|
||||
if len(schemas) > 1:
|
||||
print("\n" + "=" * 70)
|
||||
print("COMBINED SUMMARY")
|
||||
print("=" * 70)
|
||||
print(f"Total missing docstrings across all schemas: {total_missing}")
|
||||
print("=" * 70)
|
||||
|
||||
if has_errors:
|
||||
return 1
|
||||
|
||||
return 1 if total_missing > 0 else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
72
deny.toml
72
deny.toml
@@ -1,8 +1,8 @@
|
||||
[graph]
|
||||
targets = [
|
||||
"x86_64-pc-windows-msvc",
|
||||
"i686-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"i686-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc",
|
||||
]
|
||||
all-features = false
|
||||
no-default-features = false
|
||||
@@ -12,30 +12,31 @@ feature-depth = 1
|
||||
|
||||
[advisories]
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
|
||||
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" }
|
||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
|
||||
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" },
|
||||
]
|
||||
|
||||
[licenses]
|
||||
allow = [
|
||||
"0BSD",
|
||||
"Apache-2.0",
|
||||
"Artistic-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"MPL-2.0",
|
||||
"OFL-1.1",
|
||||
"Ubuntu-font-1.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"LicenseRef-Komorebi-2.0"
|
||||
"0BSD",
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"Artistic-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"MPL-2.0",
|
||||
"OFL-1.1",
|
||||
"Ubuntu-font-1.0",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"LicenseRef-Komorebi-2.0",
|
||||
]
|
||||
confidence-threshold = 0.8
|
||||
|
||||
@@ -49,6 +50,11 @@ crate = "komorebi-client"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebi-layouts"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "komorebic"
|
||||
expression = "LicenseRef-Komorebi-2.0"
|
||||
@@ -94,6 +100,11 @@ crate = "base16-egui-themes"
|
||||
expression = "MIT"
|
||||
license-files = []
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "win32-display-data"
|
||||
expression = "0BSD"
|
||||
license-files = []
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
wildcards = "allow"
|
||||
@@ -106,11 +117,12 @@ unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = [
|
||||
"https://github.com/LGUG2Z/base16-egui-themes",
|
||||
"https://github.com/LGUG2Z/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/LGUG2Z/base16-egui-themes",
|
||||
"https://github.com/LGUG2Z/windows-icons",
|
||||
"https://github.com/LGUG2Z/win32-display-data",
|
||||
"https://github.com/LGUG2Z/flavours",
|
||||
"https://github.com/LGUG2Z/base16_color_scheme",
|
||||
"https://github.com/LGUG2Z/whkd",
|
||||
"https://github.com/LGUG2Z/catppuccin-egui",
|
||||
"https://github.com/amPerl/egui-phosphor",
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
BIN
docs/assets/layout-ratios_after.png
Normal file
BIN
docs/assets/layout-ratios_after.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
BIN
docs/assets/layout-ratios_before.png
Normal file
BIN
docs/assets/layout-ratios_before.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
@@ -10,8 +10,8 @@ Options:
|
||||
Desired ease function for animation
|
||||
|
||||
[default: linear]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint,
|
||||
ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo,
|
||||
ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
|
||||
|
||||
-a, --animation-type <ANIMATION_TYPE>
|
||||
Animation type to apply the style to. If not specified, sets global style
|
||||
|
||||
12
docs/cli/cancel-preselect.md
Normal file
12
docs/cli/cancel-preselect.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# cancel-preselect
|
||||
|
||||
```
|
||||
Cancel a workspace preselect set by the preselect-direction command, if one exists
|
||||
|
||||
Usage: komorebic.exe cancel-preselect
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -12,9 +12,6 @@ Options:
|
||||
--whkd
|
||||
Enable autostart of whkd
|
||||
|
||||
--ahk
|
||||
Enable autostart of ahk
|
||||
|
||||
--bar
|
||||
Enable autostart of komorebi-bar
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@ Options:
|
||||
--whkd
|
||||
Kill whkd if it is running as a background process
|
||||
|
||||
--ahk
|
||||
Kill ahk if it is running as a background process
|
||||
|
||||
--bar
|
||||
Kill komorebi-bar if it is running as a background process
|
||||
|
||||
|
||||
16
docs/cli/license.md
Normal file
16
docs/cli/license.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# license
|
||||
|
||||
```
|
||||
Specify an email associated with an Individual Commercial Use License
|
||||
|
||||
Usage: komorebic.exe license <EMAIL>
|
||||
|
||||
Arguments:
|
||||
<EMAIL>
|
||||
Email address associated with an Individual Commercial Use License
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
16
docs/cli/preselect-direction.md
Normal file
16
docs/cli/preselect-direction.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# preselect-direction
|
||||
|
||||
```
|
||||
Preselect the specified direction for the next window to be spawned on supported layouts
|
||||
|
||||
Usage: komorebic.exe preselect-direction <OPERATION_DIRECTION>
|
||||
|
||||
Arguments:
|
||||
<OPERATION_DIRECTION>
|
||||
[possible values: left, right, up, down]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
12
docs/cli/promote-swap.md
Normal file
12
docs/cli/promote-swap.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# promote-swap
|
||||
|
||||
```
|
||||
Promote the focused window to the largest tile by swapping container indices with the largest tile
|
||||
|
||||
Usage: komorebic.exe promote-swap
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# promote
|
||||
|
||||
```
|
||||
Promote the focused window to the top of the tree
|
||||
Promote the focused window to the largest tile via container removal and re-insertion
|
||||
|
||||
Usage: komorebic.exe promote
|
||||
|
||||
|
||||
@@ -18,9 +18,6 @@ Options:
|
||||
--whkd
|
||||
Start whkd in a background process
|
||||
|
||||
--ahk
|
||||
Start autohotkey configuration file
|
||||
|
||||
--bar
|
||||
Start komorebi-bar in a background process
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@ Options:
|
||||
--whkd
|
||||
Stop whkd if it is running as a background process
|
||||
|
||||
--ahk
|
||||
Stop ahk if it is running as a background process
|
||||
|
||||
--bar
|
||||
Stop komorebi-bar if it is running as a background process
|
||||
|
||||
|
||||
200
docs/common-workflows/layout-ratios.md
Normal file
200
docs/common-workflows/layout-ratios.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Layout Ratios
|
||||
|
||||
With `komorebi` you can customize the split ratios for various layouts using
|
||||
`column_ratios` and `row_ratios` in the `layout_options` configuration.
|
||||
|
||||
## Before and After
|
||||
|
||||
BSP layout example:
|
||||
|
||||
**Before** (default 50/50 splits):
|
||||
|
||||

|
||||
|
||||
**After** (with `column_ratios: [0.7]` and `row_ratios: [0.6]`):
|
||||
|
||||

|
||||
|
||||
## Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"monitors": [
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "main",
|
||||
"layout_options": {
|
||||
"column_ratios": [0.3, 0.4],
|
||||
"row_ratios": [0.4, 0.3]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can specify up to 5 ratio values (defined by `MAX_RATIOS` constant). Each value should be between 0.1 and 0.9
|
||||
(defined by `MIN_RATIO` and `MAX_RATIO` constants). Values outside this range are automatically clamped.
|
||||
Columns or rows without a specified ratio will share the remaining space equally.
|
||||
|
||||
## Usage by Layout
|
||||
|
||||
| Layout | `column_ratios` | `row_ratios` |
|
||||
|--------|-----------------|--------------|
|
||||
| **Columns** | Width of each column | - |
|
||||
| **Rows** | - | Height of each row |
|
||||
| **Grid** | Width of each column (rows are equal height) | - |
|
||||
| **BSP** | `[0]` as horizontal split ratio | `[0]` as vertical split ratio |
|
||||
| **VerticalStack** | `[0]` as primary column width | Stack row heights |
|
||||
| **RightMainVerticalStack** | `[0]` as primary column width | Stack row heights |
|
||||
| **HorizontalStack** | Stack column widths | `[0]` as primary row height |
|
||||
| **UltrawideVerticalStack** | `[0]` center, `[1]` left column | Tertiary stack row heights |
|
||||
|
||||
## Examples
|
||||
|
||||
### Columns Layout with Custom Widths
|
||||
|
||||
Create 3 columns with 30%, 40%, and 30% widths:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout_options": {
|
||||
"column_ratios": [0.3, 0.4]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The third column automatically gets the remaining 30%.
|
||||
|
||||
### Rows Layout with Custom Heights
|
||||
|
||||
Create 3 rows with 20%, 50%, and 30% heights:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout_options": {
|
||||
"row_ratios": [0.2, 0.5]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The third row automatically gets the remaining 30%.
|
||||
|
||||
### Grid Layout with Custom Column Widths
|
||||
|
||||
Grid with custom column widths (rows within each column are always equal height):
|
||||
|
||||
```json
|
||||
{
|
||||
"layout_options": {
|
||||
"column_ratios": [0.4, 0.6]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The Grid layout only supports `column_ratios`. Rows within each column are always
|
||||
divided equally because the number of rows per column varies dynamically based on window count.
|
||||
|
||||
### VerticalStack with Custom Ratios
|
||||
|
||||
Primary column takes 60% width, and the stack rows are split 30%/70%:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout_options": {
|
||||
"column_ratios": [0.6],
|
||||
"row_ratios": [0.3]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The second row automatically gets the remaining 70%.
|
||||
|
||||
### HorizontalStack with Custom Ratios
|
||||
|
||||
Primary row takes 70% height, and the stack columns are split 40%/60%:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout_options": {
|
||||
"row_ratios": [0.7],
|
||||
"column_ratios": [0.4]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The second column automatically gets the remaining 60%.
|
||||
|
||||
### UltrawideVerticalStack with Custom Ratios
|
||||
|
||||
Center column at 50%, left column at 25% (remaining 25% goes to tertiary stack),
|
||||
with tertiary rows split 40%/60%:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout_options": {
|
||||
"column_ratios": [0.5, 0.25],
|
||||
"row_ratios": [0.4]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: The second row automatically gets the remaining 60%.
|
||||
|
||||
### BSP Layout with Custom Split Ratios
|
||||
|
||||
Use separate ratios for horizontal (left/right) and vertical (top/bottom) splits:
|
||||
|
||||
```json
|
||||
{
|
||||
"layout_options": {
|
||||
"column_ratios": [0.6],
|
||||
"row_ratios": [0.3]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `column_ratios[0]`: Controls all horizontal splits (left window gets 60%, right gets 40%)
|
||||
- `row_ratios[0]`: Controls all vertical splits (top window gets 30%, bottom gets 70%)
|
||||
|
||||
Note: BSP only uses the first value (`[0]`) from each ratio array. This single ratio is applied
|
||||
consistently to all splits of that type throughout the layout. Additional values in the arrays are ignored.
|
||||
|
||||
## Notes
|
||||
|
||||
- Ratios are clamped between 0.1 and 0.9 (prevents zero-sized windows and ensures space for other windows)
|
||||
- Default ratio is 0.5 (50%) when not specified, except for UltrawideVerticalStack secondary column which defaults to 0.25 (25%)
|
||||
- Ratios are applied **progressively** - a ratio is only used when there are more windows to place after the current one
|
||||
- The **last window always takes the remaining space**, regardless of defined ratios
|
||||
- **Ratios that would sum to 100% or more are automatically truncated** at config load time to ensure there's always space for additional windows
|
||||
- Unspecified ratios default to sharing the remaining space equally
|
||||
- You only need to specify the ratios you want to customize; trailing values can be omitted
|
||||
|
||||
## Progressive Ratio Behavior
|
||||
|
||||
Ratios are applied progressively as windows are added. For example, with `row_ratios: [0.3, 0.5]` in a VerticalStack:
|
||||
|
||||
| Windows in Stack | Row Heights |
|
||||
|------------------|-------------|
|
||||
| 1 | 100% |
|
||||
| 2 | 30%, 70% (remainder) |
|
||||
| 3 | 30%, 50%, 20% (remainder) |
|
||||
| 4 | 30%, 50%, 10%, 10% (remainder split equally) |
|
||||
| 5 | 30%, 50%, 6.67%, 6.67%, 6.67% |
|
||||
|
||||
## Automatic Ratio Truncation
|
||||
|
||||
When ratios sum to 100% (or more), they are automatically truncated at config load time.
|
||||
|
||||
For example, if you configure `column_ratios: [0.4, 0.3, 0.3]` (sums to 100%), the last ratio (0.3) is automatically removed, resulting in effectively `[0.4, 0.3]`. This ensures there's always remaining space for the last window.
|
||||
|
||||
| Configured Ratios | Effective Ratios | Reason |
|
||||
|-------------------|------------------|--------|
|
||||
| `[0.3, 0.4]` | `[0.3, 0.4]` | Sum is 0.7, below 1.0 |
|
||||
| `[0.4, 0.3, 0.3]` | `[0.4, 0.3]` | Sum would be 1.0, last ratio truncated |
|
||||
| `[0.5, 0.5]` | `[0.5]` | Sum would be 1.0, last ratio truncated |
|
||||
| `[0.6, 0.5]` | `[0.6]` | Sum would exceed 1.0, last ratio truncated |
|
||||
|
||||
This ensures the layout always fills 100% of the available space and new windows are never placed outside the visible area.
|
||||
@@ -16,6 +16,19 @@ the example files have been downloaded. For most new users this will be in the
|
||||
komorebic quickstart
|
||||
```
|
||||
|
||||
## Corporate Devices Enrolled in MDM
|
||||
|
||||
If you are using `komorebi` on a corporate device enrolled in mobile device
|
||||
management, you will receive a pop-up when you run `komorebic start` reminding
|
||||
you that the [Komorebi License](https://github.com/LGUG2Z/komorebi-license) does
|
||||
not permit any kind of commercial use.
|
||||
|
||||
You can remove this pop-up by running `komorebic license <email>` with the email
|
||||
associated with your Individual Commercial Use License. A single HTTP request
|
||||
will be sent with the given email address to verify license validity.
|
||||
|
||||
## Starting komorebi
|
||||
|
||||
With the example configurations downloaded, you can now start `komorebi`,
|
||||
`komorebi-bar` and `whkd`.
|
||||
|
||||
@@ -23,6 +36,9 @@ With the example configurations downloaded, you can now start `komorebi`,
|
||||
komorebic start --whkd --bar
|
||||
```
|
||||
|
||||
If you don't want to use the komorebi status bar, you can remove the `--bar` option
|
||||
from the above command.
|
||||
|
||||
## komorebi.json
|
||||
|
||||
The example window manager configuration sets some sane defaults and provides
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.39/schema.bar.json",
|
||||
"monitor": 0,
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.bar.json",
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
"palette": "Base16",
|
||||
@@ -32,24 +31,24 @@
|
||||
},
|
||||
{
|
||||
"Media": {
|
||||
"enable": true
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"Storage": {
|
||||
"enable": true
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"Memory": {
|
||||
"enable": true
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"Network": {
|
||||
"enable": true,
|
||||
"show_total_data_transmitted": true,
|
||||
"show_network_activity": true
|
||||
"enable": false,
|
||||
"show_activity": true,
|
||||
"show_total_activity": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.39/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.40/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
|
||||
179
flake.lock
generated
Normal file
179
flake.lock
generated
Normal file
@@ -0,0 +1,179 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1766774972,
|
||||
"narHash": "sha256-8qxEFpj4dVmIuPn9j9z6NTbU+hrcGjBOvaxTzre5HmM=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "01bc1d404a51a0a07e9d8759cd50a7903e218c82",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1761588595,
|
||||
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765835352,
|
||||
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks-nix": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765911976,
|
||||
"narHash": "sha256-t3T/xm8zstHRLx+pIHxVpQTiySbKqcQbK+r+01XVKc0=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"git-hooks-nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766870016,
|
||||
"narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1765674936,
|
||||
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-parts": "flake-parts",
|
||||
"git-hooks-nix": "git-hooks-nix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1766890375,
|
||||
"narHash": "sha256-0Zi7ChAtjq/efwQYmp7kOJPcSt6ya9ynSUe6ppgZhsQ=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "91e1f7a0017065360f447622d11b7ce6ed04772f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1766000401,
|
||||
"narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
360
flake.nix
Normal file
360
flake.nix
Normal file
@@ -0,0 +1,360 @@
|
||||
{
|
||||
description = "komorebi for Windows";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
crane.url = "github:ipetkov/crane";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
git-hooks-nix.url = "github:cachix/git-hooks.nix";
|
||||
git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-parts,
|
||||
crane,
|
||||
rust-overlay,
|
||||
...
|
||||
}:
|
||||
let
|
||||
windowsSdkVersion = "10.0.26100";
|
||||
windowsCrtVersion = "14.44.17.14";
|
||||
|
||||
mkWindowsSdk =
|
||||
pkgs:
|
||||
pkgs.stdenvNoCC.mkDerivation {
|
||||
name = "windows-sdk-${windowsSdkVersion}-crt-${windowsCrtVersion}";
|
||||
|
||||
nativeBuildInputs = [ pkgs.xwin ];
|
||||
|
||||
outputHashAlgo = "sha256";
|
||||
outputHashMode = "recursive";
|
||||
outputHash = "sha256-6cLS5q1BDRpLPScfmmKpTTEHUzsgKTKD1+mKvGX9Deo=";
|
||||
|
||||
buildCommand = ''
|
||||
export HOME=$(mktemp -d)
|
||||
xwin --accept-license \
|
||||
--sdk-version ${windowsSdkVersion} \
|
||||
--crt-version ${windowsCrtVersion} \
|
||||
splat --output $out
|
||||
'';
|
||||
};
|
||||
|
||||
mkMsvcEnv =
|
||||
{ pkgs, windowsSdk }:
|
||||
let
|
||||
clangVersion = pkgs.lib.versions.major pkgs.llvmPackages.clang.version;
|
||||
in
|
||||
{
|
||||
# linker for the windows target
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER = "lld-link";
|
||||
|
||||
# c/c++ compiler
|
||||
CC_x86_64_pc_windows_msvc = "clang-cl";
|
||||
CXX_x86_64_pc_windows_msvc = "clang-cl";
|
||||
AR_x86_64_pc_windows_msvc = "llvm-lib";
|
||||
|
||||
# IMPORTANT: libclang include path MUST come first to avoid header conflicts
|
||||
CFLAGS_x86_64_pc_windows_msvc = builtins.concatStringsSep " " [
|
||||
"--target=x86_64-pc-windows-msvc"
|
||||
"-Wno-unused-command-line-argument"
|
||||
"-fuse-ld=lld-link"
|
||||
"/imsvc${pkgs.llvmPackages.libclang.lib}/lib/clang/${clangVersion}/include"
|
||||
"/imsvc${windowsSdk}/crt/include"
|
||||
"/imsvc${windowsSdk}/sdk/include/ucrt"
|
||||
"/imsvc${windowsSdk}/sdk/include/um"
|
||||
"/imsvc${windowsSdk}/sdk/include/shared"
|
||||
];
|
||||
|
||||
CXXFLAGS_x86_64_pc_windows_msvc = builtins.concatStringsSep " " [
|
||||
"--target=x86_64-pc-windows-msvc"
|
||||
"-Wno-unused-command-line-argument"
|
||||
"-fuse-ld=lld-link"
|
||||
"/imsvc${pkgs.llvmPackages.libclang.lib}/lib/clang/${clangVersion}/include"
|
||||
"/imsvc${windowsSdk}/crt/include"
|
||||
"/imsvc${windowsSdk}/sdk/include/ucrt"
|
||||
"/imsvc${windowsSdk}/sdk/include/um"
|
||||
"/imsvc${windowsSdk}/sdk/include/shared"
|
||||
];
|
||||
|
||||
# target-specific rust flags with linker flavor and library search paths
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = builtins.concatStringsSep " " [
|
||||
"-Clinker-flavor=lld-link"
|
||||
"-Lnative=${windowsSdk}/crt/lib/x86_64"
|
||||
"-Lnative=${windowsSdk}/sdk/lib/um/x86_64"
|
||||
"-Lnative=${windowsSdk}/sdk/lib/ucrt/x86_64"
|
||||
];
|
||||
|
||||
# cargo target
|
||||
CARGO_BUILD_TARGET = "x86_64-pc-windows-msvc";
|
||||
};
|
||||
|
||||
mkKomorebiPackages =
|
||||
{ pkgs, windowsSdk }:
|
||||
let
|
||||
# toolchain with windows msvc target
|
||||
toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override {
|
||||
targets = [ "x86_64-pc-windows-msvc" ];
|
||||
};
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain toolchain;
|
||||
version = "0.1.0";
|
||||
|
||||
msvcEnv = mkMsvcEnv { inherit pkgs windowsSdk; };
|
||||
|
||||
src = pkgs.lib.cleanSourceWith {
|
||||
src = ./.;
|
||||
filter =
|
||||
path: type:
|
||||
(craneLib.filterCargoSources path type)
|
||||
|| (pkgs.lib.hasInfix "/docs/" path)
|
||||
|| (builtins.match ".*/docs/.*" path != null);
|
||||
};
|
||||
|
||||
commonArgs = {
|
||||
inherit src version;
|
||||
strictDeps = true;
|
||||
COMMIT_HASH = self.rev or (pkgs.lib.removeSuffix "-dirty" self.dirtyRev);
|
||||
|
||||
# build inputs for cross-compilation
|
||||
nativeBuildInputs = [
|
||||
pkgs.llvmPackages.clang-unwrapped
|
||||
pkgs.llvmPackages.lld
|
||||
pkgs.llvmPackages.llvm
|
||||
];
|
||||
|
||||
# cross-compilation environment
|
||||
inherit (msvcEnv)
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER
|
||||
CC_x86_64_pc_windows_msvc
|
||||
CXX_x86_64_pc_windows_msvc
|
||||
AR_x86_64_pc_windows_msvc
|
||||
CFLAGS_x86_64_pc_windows_msvc
|
||||
CXXFLAGS_x86_64_pc_windows_msvc
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS
|
||||
CARGO_BUILD_TARGET
|
||||
;
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
individualCrateArgs = commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
doCheck = false;
|
||||
doDoc = false;
|
||||
};
|
||||
|
||||
fullBuild = craneLib.buildPackage (
|
||||
individualCrateArgs
|
||||
// {
|
||||
pname = "komorebi-workspace";
|
||||
}
|
||||
);
|
||||
|
||||
extractBinary =
|
||||
binaryName:
|
||||
pkgs.runCommand "komorebi-${binaryName}"
|
||||
{
|
||||
meta = fullBuild.meta // { };
|
||||
}
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
cp ${fullBuild}/bin/${binaryName}.exe $out/bin/
|
||||
'';
|
||||
in
|
||||
{
|
||||
inherit
|
||||
craneLib
|
||||
src
|
||||
individualCrateArgs
|
||||
fullBuild
|
||||
msvcEnv
|
||||
;
|
||||
komorebi = extractBinary "komorebi";
|
||||
komorebic = extractBinary "komorebic";
|
||||
komorebic-no-console = extractBinary "komorebic-no-console";
|
||||
komorebi-bar = extractBinary "komorebi-bar";
|
||||
komorebi-gui = extractBinary "komorebi-gui";
|
||||
komorebi-shortcuts = extractBinary "komorebi-shortcuts";
|
||||
};
|
||||
|
||||
mkPkgs =
|
||||
system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
in
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
systems = [
|
||||
"aarch64-darwin"
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
|
||||
imports = [
|
||||
inputs.treefmt-nix.flakeModule
|
||||
inputs.git-hooks-nix.flakeModule
|
||||
];
|
||||
|
||||
perSystem =
|
||||
{ config, system, ... }:
|
||||
let
|
||||
pkgs = mkPkgs system;
|
||||
windowsSdk = mkWindowsSdk pkgs;
|
||||
build = mkKomorebiPackages { inherit pkgs windowsSdk; };
|
||||
|
||||
# toolchain with windows target and nightly rustfmt
|
||||
rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override {
|
||||
targets = [ "x86_64-pc-windows-msvc" ];
|
||||
};
|
||||
nightlyRustfmt = pkgs.rust-bin.nightly.latest.rustfmt;
|
||||
rustToolchainWithNightlyRustfmt = pkgs.symlinkJoin {
|
||||
name = "rust-toolchain-with-nightly-rustfmt";
|
||||
paths = [
|
||||
nightlyRustfmt
|
||||
rustToolchain
|
||||
];
|
||||
};
|
||||
nightlyToolchain = pkgs.rust-bin.nightly.latest.default.override {
|
||||
targets = [ "x86_64-pc-windows-msvc" ];
|
||||
};
|
||||
cargo-udeps = pkgs.writeShellScriptBin "cargo-udeps" ''
|
||||
export PATH="${nightlyToolchain}/bin:$PATH"
|
||||
exec ${pkgs.cargo-udeps}/bin/cargo-udeps "$@"
|
||||
'';
|
||||
in
|
||||
{
|
||||
treefmt = {
|
||||
projectRootFile = "flake.nix";
|
||||
programs = {
|
||||
deadnix.enable = true;
|
||||
just.enable = true;
|
||||
nixfmt.enable = true;
|
||||
taplo.enable = true;
|
||||
rustfmt = {
|
||||
enable = true;
|
||||
package = pkgs.rust-bin.nightly.latest.rustfmt;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
checks = {
|
||||
komorebi-workspace-clippy = build.craneLib.cargoClippy (
|
||||
build.individualCrateArgs
|
||||
// {
|
||||
cargoClippyExtraArgs = "--all-targets -- -D warnings";
|
||||
}
|
||||
);
|
||||
|
||||
komorebi-workspace-fmt = build.craneLib.cargoFmt {
|
||||
inherit (build) src;
|
||||
};
|
||||
|
||||
komorebi-workspace-toml-fmt = build.craneLib.taploFmt {
|
||||
src = pkgs.lib.sources.sourceFilesBySuffices build.src [ ".toml" ];
|
||||
};
|
||||
|
||||
komorebi-workspace-deny = build.craneLib.cargoDeny {
|
||||
inherit (build) src;
|
||||
};
|
||||
|
||||
komorebi-workspace-nextest = build.craneLib.cargoNextest build.individualCrateArgs;
|
||||
};
|
||||
|
||||
packages = {
|
||||
inherit (build)
|
||||
komorebi
|
||||
komorebic
|
||||
komorebic-no-console
|
||||
komorebi-bar
|
||||
komorebi-gui
|
||||
komorebi-shortcuts
|
||||
;
|
||||
inherit windowsSdk;
|
||||
komorebi-full = build.fullBuild;
|
||||
default = build.fullBuild;
|
||||
};
|
||||
|
||||
apps = {
|
||||
komorebi = {
|
||||
type = "app";
|
||||
program = "${build.komorebi}/bin/komorebi.exe";
|
||||
};
|
||||
komorebic = {
|
||||
type = "app";
|
||||
program = "${build.komorebic}/bin/komorebic.exe";
|
||||
};
|
||||
komorebic-no-console = {
|
||||
type = "app";
|
||||
program = "${build.komorebic-no-console}/bin/komorebic-no-console.exe";
|
||||
};
|
||||
komorebi-bar = {
|
||||
type = "app";
|
||||
program = "${build.komorebi-bar}/bin/komorebi-bar.exe";
|
||||
};
|
||||
komorebi-gui = {
|
||||
type = "app";
|
||||
program = "${build.komorebi-gui}/bin/komorebi-gui.exe";
|
||||
};
|
||||
komorebi-shortcuts = {
|
||||
type = "app";
|
||||
program = "${build.komorebi-shortcuts}/bin/komorebi-shortcuts.exe";
|
||||
};
|
||||
default = {
|
||||
type = "app";
|
||||
program = "${build.fullBuild}/bin/komorebi.exe";
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
name = "komorebi";
|
||||
|
||||
RUST_BACKTRACE = "full";
|
||||
|
||||
# cross-compilation environment
|
||||
inherit (build.msvcEnv)
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER
|
||||
CC_x86_64_pc_windows_msvc
|
||||
CXX_x86_64_pc_windows_msvc
|
||||
AR_x86_64_pc_windows_msvc
|
||||
CFLAGS_x86_64_pc_windows_msvc
|
||||
CXXFLAGS_x86_64_pc_windows_msvc
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS
|
||||
CARGO_BUILD_TARGET
|
||||
;
|
||||
|
||||
packages = [
|
||||
rustToolchainWithNightlyRustfmt
|
||||
cargo-udeps
|
||||
|
||||
# cross-compilation tooling
|
||||
pkgs.llvmPackages.clang-unwrapped # provides clang-cl
|
||||
pkgs.llvmPackages.lld # provides lld-link
|
||||
pkgs.llvmPackages.llvm # provides llvm-lib
|
||||
|
||||
pkgs.cargo-deny
|
||||
pkgs.cargo-nextest
|
||||
pkgs.cargo-outdated
|
||||
pkgs.jq
|
||||
pkgs.just
|
||||
pkgs.prettier
|
||||
];
|
||||
};
|
||||
|
||||
pre-commit = {
|
||||
check.enable = true;
|
||||
settings.hooks.treefmt = {
|
||||
enable = true;
|
||||
package = config.treefmt.build.wrapper;
|
||||
pass_filenames = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
39
justfile
39
justfile
@@ -15,6 +15,9 @@ fmt:
|
||||
prettier --write .github/FUNDING.yml
|
||||
prettier --write .github/workflows/windows.yaml
|
||||
|
||||
fix:
|
||||
cargo clippy --fix --allow-dirty
|
||||
|
||||
install-targets *targets:
|
||||
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
|
||||
|
||||
@@ -72,22 +75,38 @@ trace target $RUST_LOG="trace":
|
||||
deadlock $RUST_LOG="trace":
|
||||
cargo +stable run --bin komorebi --locked --no-default-features --features deadlock_detection
|
||||
|
||||
docgen:
|
||||
cargo run --package komorebic -- docgen
|
||||
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
|
||||
docgen starlight:
|
||||
rm {{ starlight }}/src/data/cli/windows/*.md
|
||||
cargo run --package komorebic -- docgen --output {{ starlight }}/src/data/cli/windows
|
||||
schemars-docgen ./schema.json --output {{ starlight }}/src/content/docs/reference/komorebi-windows.mdx --title "komorebi.json (Windows)" --description "komorebi for Windows configuration schema reference"
|
||||
schemars-docgen ./schema.bar.json --output {{ starlight }}/src/content/docs/reference/bar-windows.mdx --title "komorebi.bar.json (Windows)" --description "komorebi-bar for Windows configuration schema reference"
|
||||
|
||||
jsonschema:
|
||||
cargo run --package komorebic -- static-config-schema > schema.json
|
||||
cargo run --package komorebic -- application-specific-configuration-schema > schema.asc.json
|
||||
cargo run --package komorebi-bar -- --schema > schema.bar.json
|
||||
|
||||
# this part is run in a nix shell because python is a nightmare
|
||||
schemagen:
|
||||
rm -rf static-config-docs bar-config-docs
|
||||
mkdir -p static-config-docs bar-config-docs
|
||||
generate-schema-doc ./schema.json --config template_name=js_offline --config minify=false ./static-config-docs/
|
||||
generate-schema-doc ./schema.bar.json --config template_name=js_offline --config minify=false ./bar-config-docs/
|
||||
mv ./bar-config-docs/schema.bar.html ./bar-config-docs/schema.html
|
||||
version := `cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "komorebi") | .version'`
|
||||
|
||||
schemapub:
|
||||
rm -Force komorebi-schema
|
||||
mkdir -Force komorebi-schema
|
||||
cp schema.json komorebi-schema/komorebi.{{ version }}.schema.json
|
||||
cp schema.bar.json komorebi-schema/komorebi.bar.{{ version }}.schema.json
|
||||
npx wrangler pages deploy --project-name komorebi --branch main .\komorebi-schema
|
||||
|
||||
depcheck:
|
||||
cargo outdated --depth 2
|
||||
cargo +nightly udeps --quiet
|
||||
|
||||
deps:
|
||||
cargo update
|
||||
just depgen
|
||||
|
||||
depgen:
|
||||
cargo deny check
|
||||
cargo deny list --format json | jq 'del(.unlicensed)' > dependencies.json
|
||||
|
||||
procdump:
|
||||
cargo build --bin komorebi
|
||||
.\procdump.exe -ma -e -x . .\target\debug\komorebi.exe
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.39"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
# 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 = "0.10"
|
||||
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" }
|
||||
font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
lazy_static = { workspace = true }
|
||||
netdev = "0.36"
|
||||
netdev = "0.40"
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
@@ -44,4 +44,8 @@ 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",
|
||||
]
|
||||
|
||||
@@ -55,6 +55,9 @@ use komorebi_client::SocketMessage;
|
||||
use komorebi_client::VirtualDesktopNotification;
|
||||
use komorebi_themes::Base16Wrapper;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::KomobarThemeBase16;
|
||||
use komorebi_themes::KomobarThemeCatppuccin;
|
||||
use komorebi_themes::KomobarThemeCustom;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
@@ -183,12 +186,12 @@ pub fn apply_theme(
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
let (auto_select_fill, auto_select_text) = match theme {
|
||||
KomobarTheme::Catppuccin {
|
||||
KomobarTheme::Catppuccin(KomobarThemeCatppuccin {
|
||||
name: catppuccin,
|
||||
accent: catppuccin_value,
|
||||
auto_select_fill: catppuccin_auto_select_fill,
|
||||
auto_select_text: catppuccin_auto_select_text,
|
||||
} => {
|
||||
}) => {
|
||||
match catppuccin {
|
||||
Catppuccin::Frappe => {
|
||||
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
|
||||
@@ -253,12 +256,12 @@ pub fn apply_theme(
|
||||
catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())),
|
||||
)
|
||||
}
|
||||
KomobarTheme::Base16 {
|
||||
KomobarTheme::Base16(KomobarThemeBase16 {
|
||||
name: base16,
|
||||
accent: base16_value,
|
||||
auto_select_fill: base16_auto_select_fill,
|
||||
auto_select_text: base16_auto_select_text,
|
||||
} => {
|
||||
}) => {
|
||||
ctx.set_style(base16.style());
|
||||
let base16_value = base16_value.unwrap_or_default();
|
||||
let accent = base16_value.color32(Base16Wrapper::Base16(base16));
|
||||
@@ -276,12 +279,12 @@ pub fn apply_theme(
|
||||
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))),
|
||||
)
|
||||
}
|
||||
KomobarTheme::Custom {
|
||||
KomobarTheme::Custom(KomobarThemeCustom {
|
||||
colours,
|
||||
accent: base16_value,
|
||||
auto_select_fill: base16_auto_select_fill,
|
||||
auto_select_text: base16_auto_select_text,
|
||||
} => {
|
||||
}) => {
|
||||
let background = colours.background();
|
||||
ctx.set_style(colours.style());
|
||||
let base16_value = base16_value.unwrap_or_default();
|
||||
@@ -450,10 +453,11 @@ impl Komobar {
|
||||
self.right_widgets = right_widgets;
|
||||
|
||||
let (usr_monitor_index, config_work_area_offset) = match &self.config.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
|
||||
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
|
||||
(monitor_config.index, monitor_config.work_area_offset)
|
||||
}
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None),
|
||||
None => (0, None),
|
||||
};
|
||||
|
||||
let mapped_info = self.monitor_info.as_ref().map(|info| {
|
||||
@@ -863,9 +867,13 @@ impl eframe::App for Komobar {
|
||||
Ok(KomorebiEvent::Notification(notification)) => {
|
||||
let state = ¬ification.state;
|
||||
let usr_monitor_index = match &self.config.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
|
||||
MonitorConfigOrIndex::Index(idx) => *idx,
|
||||
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
|
||||
monitor_config.index
|
||||
}
|
||||
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
let monitor_index = state.monitor_usr_idx_map.get(&usr_monitor_index).copied();
|
||||
self.monitor_index = monitor_index;
|
||||
let mut should_apply_config = false;
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::widgets::widget::WidgetConfig;
|
||||
use eframe::egui::Pos2;
|
||||
use eframe::egui::TextBuffer;
|
||||
use eframe::egui::Vec2;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::Rect;
|
||||
use komorebi_client::SocketMessage;
|
||||
@@ -16,9 +15,10 @@ use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.39`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.40`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar height (default: 50)
|
||||
/// Bar height
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50)))]
|
||||
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,20 +76,26 @@ pub struct KomobarConfig {
|
||||
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
|
||||
pub frame: Option<FrameConfig>,
|
||||
/// The monitor index or the full monitor options
|
||||
pub monitor: MonitorConfigOrIndex,
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = MonitorConfigOrIndex::Index(0))))]
|
||||
pub monitor: Option<MonitorConfigOrIndex>,
|
||||
/// Font family
|
||||
pub font_family: Option<String>,
|
||||
/// Font size (default: 12.5)
|
||||
/// Font size
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12.5)))]
|
||||
pub font_size: Option<f32>,
|
||||
/// Scale of the icons relative to the font_size [[1.0-2.0]]. (default: 1.4)
|
||||
/// Scale of the icons relative to the font_size [[1.0-2.0]]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 1.4)))]
|
||||
pub icon_scale: Option<f32>,
|
||||
/// Max label width before text truncation (default: 400.0)
|
||||
/// Max label width before text truncation
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 400.0)))]
|
||||
pub max_label_width: Option<f32>,
|
||||
/// Theme
|
||||
pub theme: Option<KomobarTheme>,
|
||||
/// Alpha value for the color transparency [[0-255]] (default: 200)
|
||||
/// Alpha value for the color transparency [[0-255]]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 200)))]
|
||||
pub transparency_alpha: Option<u8>,
|
||||
/// Spacing between widgets (default: 10.0)
|
||||
/// Spacing between widgets
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10.0)))]
|
||||
pub widget_spacing: Option<f32>,
|
||||
/// Visual grouping for widgets
|
||||
pub grouping: Option<Grouping>,
|
||||
@@ -145,6 +151,7 @@ impl KomobarConfig {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Position configuration
|
||||
pub struct PositionConfig {
|
||||
/// The desired starting position of the bar (0,0 = top left of the screen)
|
||||
#[serde(alias = "position")]
|
||||
@@ -156,6 +163,7 @@ pub struct PositionConfig {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Frame configuration
|
||||
pub struct FrameConfig {
|
||||
/// Margin inside the painted frame
|
||||
pub inner_margin: Position,
|
||||
@@ -164,6 +172,7 @@ pub struct FrameConfig {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Monitor configuration or monitor index
|
||||
pub enum MonitorConfigOrIndex {
|
||||
/// The monitor index where you want the bar to show
|
||||
Index(usize),
|
||||
@@ -173,6 +182,7 @@ pub enum MonitorConfigOrIndex {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Monitor configuration
|
||||
pub struct MonitorConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub index: usize,
|
||||
@@ -190,9 +200,13 @@ pub type Margin = SpacingKind;
|
||||
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
|
||||
// `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the
|
||||
// individual values.
|
||||
/// Spacing kind
|
||||
pub enum SpacingKind {
|
||||
/// Spacing applied to all sides
|
||||
All(f32),
|
||||
/// Individual spacing applied to each side
|
||||
Individual(IndividualSpacingConfig),
|
||||
/// Grouped vertical and horizontal spacing
|
||||
Grouped(GroupedSpacingConfig),
|
||||
}
|
||||
|
||||
@@ -237,25 +251,36 @@ impl SpacingKind {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Grouped vertical and horizontal spacing
|
||||
pub struct GroupedSpacingConfig {
|
||||
/// Vertical grouped spacing
|
||||
pub vertical: Option<GroupedSpacingOptions>,
|
||||
/// Horizontal grouped spacing
|
||||
pub horizontal: Option<GroupedSpacingOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Grouped spacing options
|
||||
pub enum GroupedSpacingOptions {
|
||||
/// Symmetrical grouped spacing
|
||||
Symmetrical(f32),
|
||||
/// Split grouped spacing
|
||||
Split(f32, f32),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Individual spacing configuration
|
||||
pub struct IndividualSpacingConfig {
|
||||
/// Spacing for the top
|
||||
pub top: f32,
|
||||
/// Spacing for the bottom
|
||||
pub bottom: f32,
|
||||
/// Spacing for the left
|
||||
pub left: f32,
|
||||
/// Spacing for the right
|
||||
pub right: f32,
|
||||
}
|
||||
|
||||
@@ -335,6 +360,7 @@ 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:
|
||||
@@ -379,10 +405,13 @@ pub enum MouseMessage {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Komorebi socket mouse message
|
||||
pub struct KomorebiMouseMessage {
|
||||
/// Send the FocusMonitorAtCursor message (default:true)
|
||||
/// Send the FocusMonitorAtCursor message
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||
pub focus_monitor_at_cursor: Option<bool>,
|
||||
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message (default:true)
|
||||
/// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||
pub ignore_mouse_follows_focus: Option<bool>,
|
||||
/// The message to send to the komorebi client
|
||||
pub message: komorebi_client::SocketMessage,
|
||||
@@ -390,6 +419,7 @@ 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>,
|
||||
@@ -402,14 +432,16 @@ 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 (default: 30)
|
||||
/// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 30.0)))]
|
||||
pub vertical_scroll_threshold: Option<f32>,
|
||||
/// Command to send on scrolling up (every tick)
|
||||
pub on_scroll_up: Option<MouseMessage>,
|
||||
/// Command to send on scrolling down (every tick)
|
||||
pub on_scroll_down: Option<MouseMessage>,
|
||||
|
||||
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen (default: 30)
|
||||
/// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 30.0)))]
|
||||
pub horizontal_scroll_threshold: Option<f32>,
|
||||
/// Command to send on scrolling left (every tick)
|
||||
pub on_scroll_left: Option<MouseMessage>,
|
||||
@@ -495,6 +527,7 @@ impl KomobarConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Position
|
||||
pub struct Position {
|
||||
/// X coordinate
|
||||
pub x: f32,
|
||||
@@ -520,71 +553,11 @@ impl From<Position> for Pos2 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
pub enum KomobarTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
auto_select_fill: Option<komorebi_themes::CatppuccinValue>,
|
||||
auto_select_text: Option<komorebi_themes::CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
name: komorebi_themes::Base16,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_fill: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_text: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
/// A custom Base16 theme
|
||||
Custom {
|
||||
/// Colours of the custom Base16 theme palette
|
||||
colours: Box<komorebi_themes::Base16ColourPalette>,
|
||||
accent: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_fill: Option<komorebi_themes::Base16Value>,
|
||||
auto_select_text: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<KomorebiTheme> for KomobarTheme {
|
||||
fn from(value: KomorebiTheme) -> Self {
|
||||
match value {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name, bar_accent, ..
|
||||
} => Self::Catppuccin {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
KomorebiTheme::Base16 {
|
||||
name, bar_accent, ..
|
||||
} => Self::Base16 {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
KomorebiTheme::Custom {
|
||||
colours,
|
||||
bar_accent,
|
||||
..
|
||||
} => Self::Custom {
|
||||
colours,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use komorebi_themes::KomobarTheme;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Label prefix
|
||||
pub enum LabelPrefix {
|
||||
/// Show no prefix
|
||||
None,
|
||||
@@ -598,6 +571,7 @@ pub enum LabelPrefix {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Display format
|
||||
pub enum DisplayFormat {
|
||||
/// Show only icon
|
||||
Icon,
|
||||
@@ -612,9 +586,10 @@ pub enum DisplayFormat {
|
||||
}
|
||||
|
||||
macro_rules! extend_enum {
|
||||
($existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
|
||||
($(#[$type_meta:meta])* $existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
|
||||
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
$(#[$type_meta])*
|
||||
pub enum $new_enum {
|
||||
// Add new variants
|
||||
$(
|
||||
@@ -635,7 +610,9 @@ macro_rules! extend_enum {
|
||||
};
|
||||
}
|
||||
|
||||
extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
|
||||
extend_enum!(
|
||||
/// Workspaces display format
|
||||
DisplayFormat, WorkspacesDisplayFormat, {
|
||||
/// Show all icons only
|
||||
AllIcons,
|
||||
/// Show both all icons and text
|
||||
@@ -644,6 +621,26 @@ extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
|
||||
AllIconsAndTextOnSelected,
|
||||
});
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Media widget display format
|
||||
pub enum MediaDisplayFormat {
|
||||
/// Show only the media info icon
|
||||
Icon,
|
||||
/// Show only the media info text (artist - title)
|
||||
Text,
|
||||
/// Show both icon and text
|
||||
IconAndText,
|
||||
/// Show only the control buttons (previous, play/pause, next)
|
||||
ControlsOnly,
|
||||
/// Show icon with control buttons
|
||||
IconAndControls,
|
||||
/// Show text with control buttons
|
||||
TextAndControls,
|
||||
/// Show icon, text, and control buttons
|
||||
Full,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -103,7 +103,7 @@ fn process_hwnd() -> Option<isize> {
|
||||
}
|
||||
|
||||
pub enum KomorebiEvent {
|
||||
Notification(komorebi_client::Notification),
|
||||
Notification(Box<komorebi_client::Notification>),
|
||||
Reconnect,
|
||||
}
|
||||
|
||||
@@ -114,15 +114,8 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
if opts.schema {
|
||||
let settings = schemars::r#gen::SchemaSettings::default().with(|s| {
|
||||
s.option_nullable = false;
|
||||
s.option_add_null_type = false;
|
||||
s.inline_subschemas = true;
|
||||
});
|
||||
|
||||
let generator = settings.into_generator();
|
||||
let socket_message = generator.into_root_schema_for::<KomobarConfig>();
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
let bar_config = schemars::schema_for!(KomobarConfig);
|
||||
let schema = serde_json::to_string_pretty(&bar_config)?;
|
||||
|
||||
println!("{schema}");
|
||||
std::process::exit(0);
|
||||
@@ -223,11 +216,13 @@ fn main() -> color_eyre::Result<()> {
|
||||
)?)?;
|
||||
|
||||
let (usr_monitor_index, work_area_offset) = match &config.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => {
|
||||
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => {
|
||||
(monitor_config.index, monitor_config.work_area_offset)
|
||||
}
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None),
|
||||
None => (0, None),
|
||||
};
|
||||
|
||||
let monitor_index = state
|
||||
.monitor_usr_idx_map
|
||||
.get(&usr_monitor_index)
|
||||
@@ -381,7 +376,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
Ok(notification) => {
|
||||
tracing::debug!("received notification from komorebi");
|
||||
|
||||
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) {
|
||||
if let Err(error) = tx_gui.send(KomorebiEvent::Notification(Box::new(notification))) {
|
||||
tracing::error!("could not send komorebi notification update to gui thread: {error}")
|
||||
}
|
||||
|
||||
|
||||
@@ -27,18 +27,24 @@ static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "kind")]
|
||||
/// Grouping
|
||||
pub enum Grouping {
|
||||
/// No grouping is applied
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "None"))]
|
||||
None,
|
||||
/// Widgets are grouped as a whole
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Bar"))]
|
||||
Bar(GroupingConfig),
|
||||
/// Widgets are grouped by alignment
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Alignment"))]
|
||||
Alignment(GroupingConfig),
|
||||
/// Widgets are grouped individually
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Widget"))]
|
||||
Widget(GroupingConfig),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Render configuration
|
||||
pub struct RenderConfig {
|
||||
/// Komorebi monitor index of the monitor on which to render the bar
|
||||
pub monitor_idx: usize,
|
||||
@@ -93,8 +99,9 @@ impl RenderExt for &KomobarConfig {
|
||||
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
|
||||
|
||||
let monitor_idx = match &self.monitor {
|
||||
MonitorConfigOrIndex::MonitorConfig(monitor_config) => monitor_config.index,
|
||||
MonitorConfigOrIndex::Index(idx) => *idx,
|
||||
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => monitor_config.index,
|
||||
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
|
||||
@@ -356,6 +363,7 @@ impl RenderConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Grouping configuration
|
||||
pub struct GroupingConfig {
|
||||
/// Styles for the grouping
|
||||
pub style: Option<GroupingStyle>,
|
||||
@@ -367,7 +375,9 @@ pub struct GroupingConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Grouping Style
|
||||
pub enum GroupingStyle {
|
||||
/// Default
|
||||
#[serde(alias = "CtByte")]
|
||||
Default,
|
||||
/// A shadow is added under the default group. (blur: 4, offset: x-1 y-1, spread: 3)
|
||||
@@ -389,8 +399,9 @@ pub enum GroupingStyle {
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Rounding configuration
|
||||
pub enum RoundingConfig {
|
||||
/// All 4 corners are the same
|
||||
/// All 4 corners are the same
|
||||
Same(f32),
|
||||
/// All 4 corners are custom. Order: NW, NE, SW, SE
|
||||
Individual([f32; 4]),
|
||||
|
||||
@@ -34,6 +34,7 @@ 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,
|
||||
@@ -44,13 +45,14 @@ 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<DisplayFormat>,
|
||||
pub display: Option<ApplicationsDisplayFormat>,
|
||||
/// List of configured applications to display.
|
||||
pub items: Vec<AppConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Application button configuration
|
||||
pub struct AppConfig {
|
||||
/// Whether to enable this application button (optional).
|
||||
/// Inherits from the global `Applications` setting if omitted.
|
||||
@@ -67,12 +69,13 @@ 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<DisplayFormat>,
|
||||
pub display: Option<ApplicationsDisplayFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum DisplayFormat {
|
||||
/// Applications widget display format
|
||||
pub enum ApplicationsDisplayFormat {
|
||||
/// Show only the application icon.
|
||||
#[default]
|
||||
Icon,
|
||||
@@ -168,7 +171,7 @@ pub struct App {
|
||||
/// Command to execute when the application is launched.
|
||||
pub command: UserCommand,
|
||||
/// Display format (icon, text, or both).
|
||||
pub display: DisplayFormat,
|
||||
pub display: ApplicationsDisplayFormat,
|
||||
/// Whether to show the launch command on hover.
|
||||
pub show_command_on_hover: bool,
|
||||
}
|
||||
@@ -183,9 +186,9 @@ impl App {
|
||||
ui.spacing_mut().item_spacing = Vec2::splat(4.0);
|
||||
|
||||
match self.display {
|
||||
DisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
|
||||
DisplayFormat::Text => self.draw_name(ui),
|
||||
DisplayFormat::IconAndText => {
|
||||
ApplicationsDisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
|
||||
ApplicationsDisplayFormat::Text => self.draw_name(ui),
|
||||
ApplicationsDisplayFormat::IconAndText => {
|
||||
self.draw_icon(ctx, ui, icon_config);
|
||||
self.draw_name(ui);
|
||||
}
|
||||
|
||||
@@ -19,12 +19,14 @@ use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Battery widget configuration
|
||||
pub struct BatteryConfig {
|
||||
/// Enable the Battery widget
|
||||
pub enable: bool,
|
||||
/// Hide the widget if the battery is at full charge
|
||||
pub hide_on_full_charge: Option<bool>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
/// Data refresh interval in seconds
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
|
||||
@@ -18,10 +18,12 @@ use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// CPU widget configuration
|
||||
pub struct CpuConfig {
|
||||
/// Enable the Cpu widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
/// Data refresh interval in seconds
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
|
||||
@@ -62,6 +62,7 @@ impl CustomModifiers {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Date widget configuration
|
||||
pub struct DateConfig {
|
||||
/// Enable the Date widget
|
||||
pub enable: bool,
|
||||
@@ -104,6 +105,7 @@ impl From<DateConfig> for Date {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Date widget format
|
||||
pub enum DateFormat {
|
||||
/// Month/Date/Year format (09/08/24)
|
||||
MonthDateYear,
|
||||
@@ -114,8 +116,10 @@ pub enum DateFormat {
|
||||
/// Day Date Month Year format (8 September 2024)
|
||||
DayDateMonthYear,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
Custom(String),
|
||||
/// Custom format with modifiers
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "CustomModifiers"))]
|
||||
CustomModifiers(CustomModifiers),
|
||||
}
|
||||
|
||||
|
||||
@@ -21,15 +21,16 @@ use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyboardLayout;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
|
||||
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
|
||||
const ERROR_TEXT: &str = "Error";
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Keyboard widget configuration
|
||||
pub struct KeyboardConfig {
|
||||
/// Enable the Input widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 1 second)
|
||||
/// Data refresh interval
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
@@ -37,9 +38,7 @@ pub struct KeyboardConfig {
|
||||
|
||||
impl From<KeyboardConfig> for Keyboard {
|
||||
fn from(value: KeyboardConfig) -> Self {
|
||||
let data_refresh_interval = value
|
||||
.data_refresh_interval
|
||||
.unwrap_or(DEFAULT_DATA_REFRESH_INTERVAL);
|
||||
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
|
||||
@@ -49,6 +49,7 @@ use std::sync::atomic::Ordering;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Komorebi widget configuration
|
||||
pub struct KomorebiConfig {
|
||||
/// Configure the Workspaces widget
|
||||
pub workspaces: Option<KomorebiWorkspacesConfig>,
|
||||
@@ -67,6 +68,7 @@ pub struct KomorebiConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Komorebi widget workspaces configuration
|
||||
pub struct KomorebiWorkspacesConfig {
|
||||
/// Enable the Komorebi Workspaces widget
|
||||
pub enable: bool,
|
||||
@@ -78,6 +80,7 @@ pub struct KomorebiWorkspacesConfig {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Komorebi widget layout configuration
|
||||
pub struct KomorebiLayoutConfig {
|
||||
/// Enable the Komorebi Layout widget
|
||||
pub enable: bool,
|
||||
@@ -89,6 +92,7 @@ pub struct KomorebiLayoutConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Komorebi widget workspace layer configuration
|
||||
pub struct KomorebiWorkspaceLayerConfig {
|
||||
/// Enable the Komorebi Workspace Layer widget
|
||||
pub enable: bool,
|
||||
@@ -100,10 +104,12 @@ pub struct KomorebiWorkspaceLayerConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Komorebi widget focused container configuration
|
||||
pub struct KomorebiFocusedContainerConfig {
|
||||
/// Enable the Komorebi Focused Container widget
|
||||
pub enable: bool,
|
||||
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused container)
|
||||
/// DEPRECATED: use `display` instead (Show the icon of the currently focused container)
|
||||
#[deprecated(note = "Use `display` instead")]
|
||||
pub show_icon: Option<bool>,
|
||||
/// Display format of the currently focused container
|
||||
pub display: Option<DisplayFormat>,
|
||||
@@ -111,6 +117,7 @@ pub struct KomorebiFocusedContainerConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Komorebi widget locked container configuration
|
||||
pub struct KomorebiLockedContainerConfig {
|
||||
/// Enable the Komorebi Locked Container widget
|
||||
pub enable: bool,
|
||||
@@ -122,6 +129,7 @@ pub struct KomorebiLockedContainerConfig {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Komorebi widget configuration switcher configuration
|
||||
pub struct KomorebiConfigurationSwitcherConfig {
|
||||
/// Enable the Komorebi Configurations widget
|
||||
pub enable: bool,
|
||||
@@ -506,7 +514,9 @@ impl FocusedContainerBar {
|
||||
if !value.enable {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Handle legacy setting - convert show_icon to display format
|
||||
#[allow(deprecated)]
|
||||
let format = value
|
||||
.display
|
||||
.unwrap_or(if value.show_icon.unwrap_or(false) {
|
||||
|
||||
@@ -26,11 +26,18 @@ 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,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::MAX_LABEL_WIDTH;
|
||||
use crate::bar::Alignment;
|
||||
use crate::config::MediaDisplayFormat;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::selected_frame::SelectableFrame;
|
||||
use crate::ui::CustomUi;
|
||||
@@ -14,30 +16,39 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
|
||||
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Media widget configuration
|
||||
pub struct MediaConfig {
|
||||
/// Enable the Media widget
|
||||
pub enable: bool,
|
||||
/// Display format of the media widget (defaults to IconAndText)
|
||||
pub display: Option<MediaDisplayFormat>,
|
||||
}
|
||||
|
||||
impl From<MediaConfig> for Media {
|
||||
fn from(value: MediaConfig) -> Self {
|
||||
Self::new(value.enable)
|
||||
Self::new(
|
||||
value.enable,
|
||||
value.display.unwrap_or(MediaDisplayFormat::IconAndText),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Media {
|
||||
pub enable: bool,
|
||||
pub display: MediaDisplayFormat,
|
||||
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
|
||||
}
|
||||
|
||||
impl Media {
|
||||
pub fn new(enable: bool) -> Self {
|
||||
pub fn new(enable: bool, display: MediaDisplayFormat) -> Self {
|
||||
Self {
|
||||
enable,
|
||||
display,
|
||||
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
|
||||
.unwrap()
|
||||
.join()
|
||||
@@ -53,6 +64,58 @@ impl Media {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous(&self) {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(op) = session.TrySkipPreviousAsync()
|
||||
{
|
||||
op.join().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(op) = session.TrySkipNextAsync()
|
||||
{
|
||||
op.join().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_playing(&self) -> bool {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(info) = session.GetPlaybackInfo()
|
||||
&& let Ok(status) = info.PlaybackStatus()
|
||||
{
|
||||
return status == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_previous_enabled(&self) -> bool {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(info) = session.GetPlaybackInfo()
|
||||
&& let Ok(controls) = info.Controls()
|
||||
&& let Ok(enabled) = controls.IsPreviousEnabled()
|
||||
{
|
||||
return enabled;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_next_enabled(&self) -> bool {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(info) = session.GetPlaybackInfo()
|
||||
&& let Ok(controls) = info.Controls()
|
||||
&& let Ok(enabled) = controls.IsNextEnabled()
|
||||
{
|
||||
return enabled;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn has_session(&self) -> bool {
|
||||
self.session_manager.GetCurrentSession().is_ok()
|
||||
}
|
||||
|
||||
fn output(&mut self) -> String {
|
||||
if let Ok(session) = self.session_manager.GetCurrentSession()
|
||||
&& let Ok(operation) = session.TryGetMediaPropertiesAsync()
|
||||
@@ -77,28 +140,96 @@ impl Media {
|
||||
impl BarWidget for Media {
|
||||
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
|
||||
if self.enable {
|
||||
// Don't render if there's no active media session
|
||||
if !self.has_session() {
|
||||
return;
|
||||
}
|
||||
|
||||
let output = self.output();
|
||||
if !output.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
|
||||
let show_icon = matches!(
|
||||
self.display,
|
||||
MediaDisplayFormat::Icon
|
||||
| MediaDisplayFormat::IconAndText
|
||||
| MediaDisplayFormat::IconAndControls
|
||||
| MediaDisplayFormat::Full
|
||||
);
|
||||
let show_text = matches!(
|
||||
self.display,
|
||||
MediaDisplayFormat::Text
|
||||
| MediaDisplayFormat::IconAndText
|
||||
| MediaDisplayFormat::TextAndControls
|
||||
| MediaDisplayFormat::Full
|
||||
);
|
||||
let show_controls = matches!(
|
||||
self.display,
|
||||
MediaDisplayFormat::ControlsOnly
|
||||
| MediaDisplayFormat::IconAndControls
|
||||
| MediaDisplayFormat::TextAndControls
|
||||
| MediaDisplayFormat::Full
|
||||
);
|
||||
|
||||
// Don't render if there's no media info and we're not showing controls-only
|
||||
if output.is_empty() && !show_controls {
|
||||
return;
|
||||
}
|
||||
|
||||
let icon_font_id = config.icon_font_id.clone();
|
||||
let text_font_id = config.text_font_id.clone();
|
||||
let icon_color = ctx.style().visuals.selection.stroke.color;
|
||||
let text_color = ctx.style().visuals.text_color();
|
||||
|
||||
let mut layout_job = LayoutJob::default();
|
||||
|
||||
if show_icon {
|
||||
layout_job = LayoutJob::simple(
|
||||
egui_phosphor::regular::HEADPHONES.to_string(),
|
||||
config.icon_font_id.clone(),
|
||||
ctx.style().visuals.selection.stroke.color,
|
||||
icon_font_id.clone(),
|
||||
icon_color,
|
||||
100.0,
|
||||
);
|
||||
}
|
||||
|
||||
if show_text {
|
||||
layout_job.append(
|
||||
&output,
|
||||
10.0,
|
||||
if show_icon { 10.0 } else { 0.0 },
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
color: ctx.style().visuals.text_color(),
|
||||
font_id: text_font_id,
|
||||
color: text_color,
|
||||
valign: Align::Center,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if SelectableFrame::new(false)
|
||||
let is_playing = self.is_playing();
|
||||
let is_previous_enabled = self.is_previous_enabled();
|
||||
let is_next_enabled = self.is_next_enabled();
|
||||
let disabled_color = text_color.gamma_multiply(0.5);
|
||||
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
|
||||
|
||||
let prev_color = if is_previous_enabled {
|
||||
text_color
|
||||
} else {
|
||||
disabled_color
|
||||
};
|
||||
|
||||
let next_color = if is_next_enabled {
|
||||
text_color
|
||||
} else {
|
||||
disabled_color
|
||||
};
|
||||
|
||||
let play_pause_icon = if is_playing {
|
||||
egui_phosphor::regular::PAUSE
|
||||
} else {
|
||||
egui_phosphor::regular::PLAY
|
||||
};
|
||||
|
||||
let show_label = |ui: &mut Ui| {
|
||||
if (show_icon || show_text)
|
||||
&& SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
let available_height = ui.available_height();
|
||||
let mut custom_ui = CustomUi(ui);
|
||||
@@ -108,15 +239,95 @@ impl BarWidget for Media {
|
||||
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
|
||||
available_height,
|
||||
),
|
||||
Label::new(layout_job).selectable(false).truncate(),
|
||||
Label::new(layout_job.clone()).selectable(false).truncate(),
|
||||
)
|
||||
})
|
||||
.on_hover_text(&output)
|
||||
.clicked()
|
||||
{
|
||||
self.toggle();
|
||||
{
|
||||
self.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
let show_previous = |ui: &mut Ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(LayoutJob::simple(
|
||||
egui_phosphor::regular::SKIP_BACK.to_string(),
|
||||
icon_font_id.clone(),
|
||||
prev_color,
|
||||
100.0,
|
||||
))
|
||||
.selectable(false),
|
||||
)
|
||||
})
|
||||
.clicked()
|
||||
&& is_previous_enabled
|
||||
{
|
||||
self.previous();
|
||||
}
|
||||
};
|
||||
|
||||
let show_play_pause = |ui: &mut Ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(LayoutJob::simple(
|
||||
play_pause_icon.to_string(),
|
||||
icon_font_id.clone(),
|
||||
text_color,
|
||||
100.0,
|
||||
))
|
||||
.selectable(false),
|
||||
)
|
||||
})
|
||||
.on_hover_text(&output)
|
||||
.clicked()
|
||||
{
|
||||
self.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
let show_next = |ui: &mut Ui| {
|
||||
if SelectableFrame::new(false)
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
Label::new(LayoutJob::simple(
|
||||
egui_phosphor::regular::SKIP_FORWARD.to_string(),
|
||||
icon_font_id.clone(),
|
||||
next_color,
|
||||
100.0,
|
||||
))
|
||||
.selectable(false),
|
||||
)
|
||||
})
|
||||
.clicked()
|
||||
&& is_next_enabled
|
||||
{
|
||||
self.next();
|
||||
}
|
||||
};
|
||||
|
||||
config.apply_on_widget(false, ui, |ui| {
|
||||
if is_reversed {
|
||||
// Right panel renders right-to-left, so reverse order
|
||||
if show_controls {
|
||||
show_next(ui);
|
||||
show_play_pause(ui);
|
||||
show_previous(ui);
|
||||
}
|
||||
});
|
||||
}
|
||||
show_label(ui);
|
||||
} else {
|
||||
// Left/center panel renders left-to-right, normal order
|
||||
show_label(ui);
|
||||
if show_controls {
|
||||
show_previous(ui);
|
||||
show_play_pause(ui);
|
||||
show_next(ui);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ use sysinfo::System;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Memory widget configuration
|
||||
pub struct MemoryConfig {
|
||||
/// Enable the Memory widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
/// Data refresh interval in seconds
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
|
||||
@@ -15,12 +15,18 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use sysinfo::Networks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Network widget configuration
|
||||
pub struct NetworkConfig {
|
||||
/// Enable the Network widget
|
||||
pub enable: bool,
|
||||
@@ -35,7 +41,8 @@ pub struct NetworkConfig {
|
||||
/// Characters to reserve for received and transmitted activity
|
||||
#[serde(alias = "network_activity_fill_characters")]
|
||||
pub activity_left_padding: Option<usize>,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
/// Data refresh interval in seconds
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
@@ -45,6 +52,7 @@ pub struct NetworkConfig {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Network select configuration
|
||||
pub struct NetworkSelectConfig {
|
||||
/// Select the total received data when it's over this value
|
||||
pub total_received_over: Option<u64>,
|
||||
@@ -59,28 +67,34 @@ 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(10);
|
||||
let data_refresh_interval = value
|
||||
.data_refresh_interval
|
||||
.unwrap_or(default_refresh_interval);
|
||||
|
||||
Self {
|
||||
enable: value.enable,
|
||||
show_total_activity: value.show_total_activity,
|
||||
show_activity: value.show_activity,
|
||||
show_default_interface: value.show_default_interface.unwrap_or(true),
|
||||
networks_network_activity: Networks::new_with_refreshed_list(),
|
||||
default_interface: String::new(),
|
||||
networks_network_activity: Arc::new(Mutex::new(Networks::new_with_refreshed_list())),
|
||||
default_interface: Arc::new(Mutex::new(String::new())),
|
||||
interface_generation: Arc::new(AtomicU64::new(0)),
|
||||
default_refresh_interval,
|
||||
data_refresh_interval,
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
|
||||
auto_select: value.auto_select,
|
||||
activity_left_padding: value.activity_left_padding.unwrap_or_default(),
|
||||
last_updated_default_interface: Instant::now()
|
||||
last_update_request_default_interface: Instant::now()
|
||||
.checked_sub(Duration::from_secs(default_refresh_interval))
|
||||
.unwrap(),
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,63 +104,82 @@ pub struct Network {
|
||||
pub show_total_activity: bool,
|
||||
pub show_activity: bool,
|
||||
pub show_default_interface: bool,
|
||||
networks_network_activity: Networks,
|
||||
networks_network_activity: Arc<Mutex<Networks>>,
|
||||
default_refresh_interval: u64,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
auto_select: Option<NetworkSelectConfig>,
|
||||
default_interface: String,
|
||||
last_updated_default_interface: Instant,
|
||||
last_state_total_activity: Vec<NetworkReading>,
|
||||
last_state_activity: Vec<NetworkReading>,
|
||||
last_updated_network_activity: Instant,
|
||||
default_interface: Arc<Mutex<String>>,
|
||||
interface_generation: Arc<AtomicU64>,
|
||||
last_update_request_default_interface: Instant,
|
||||
activity_generation: Arc<AtomicU64>,
|
||||
last_state_activity: Arc<Mutex<Vec<NetworkReading>>>,
|
||||
last_state_total_activity: Arc<Mutex<Vec<NetworkReading>>>,
|
||||
last_update_request_network_activity: Arc<Mutex<Instant>>,
|
||||
activity_left_padding: usize,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
fn default_interface(&mut self) {
|
||||
let now = Instant::now();
|
||||
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);
|
||||
|
||||
if now.duration_since(self.last_updated_default_interface)
|
||||
> Duration::from_secs(self.default_refresh_interval)
|
||||
{
|
||||
thread::spawn(move || {
|
||||
if let Ok(interface) = netdev::get_default_interface()
|
||||
&& let Some(friendly_name) = &interface.friendly_name
|
||||
{
|
||||
self.default_interface.clone_from(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();
|
||||
}
|
||||
}
|
||||
|
||||
self.last_updated_default_interface = now;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let mut activity = self.last_state_activity.clone();
|
||||
let mut total_activity = self.last_state_total_activity.clone();
|
||||
fn default_interface(&mut self) -> String {
|
||||
let current = self.default_interface.lock().unwrap().clone();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(self.last_updated_network_activity)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
if now.duration_since(self.last_update_request_default_interface)
|
||||
> Duration::from_secs(self.default_refresh_interval)
|
||||
{
|
||||
activity.clear();
|
||||
total_activity.clear();
|
||||
self.last_update_request_default_interface = now;
|
||||
self.update_default_interface_async();
|
||||
}
|
||||
|
||||
current
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut activity = Vec::new();
|
||||
let mut total_activity = Vec::new();
|
||||
|
||||
if let Ok(interface) = netdev::get_default_interface()
|
||||
&& let Some(friendly_name) = &interface.friendly_name
|
||||
&& let Ok(mut networks) = networks_network_activity_arc.lock()
|
||||
{
|
||||
self.default_interface.clone_from(friendly_name);
|
||||
networks.refresh(true);
|
||||
|
||||
self.networks_network_activity.refresh(true);
|
||||
|
||||
for (interface_name, data) in &self.networks_network_activity {
|
||||
for (interface_name, data) in &*networks {
|
||||
if friendly_name.eq(interface_name) {
|
||||
if self.show_activity {
|
||||
if 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,
|
||||
);
|
||||
Network::to_pretty_bytes(data.received(), data_refresh_interval);
|
||||
let transmitted =
|
||||
Network::to_pretty_bytes(data.transmitted(), data_refresh_interval);
|
||||
|
||||
activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Speed,
|
||||
@@ -155,26 +188,55 @@ impl Network {
|
||||
));
|
||||
}
|
||||
|
||||
if self.show_total_activity {
|
||||
let total_received = Self::to_pretty_bytes(data.total_received(), 1);
|
||||
if show_total_activity {
|
||||
let total_received = Network::to_pretty_bytes(data.total_received(), 1);
|
||||
let total_transmitted =
|
||||
Self::to_pretty_bytes(data.total_transmitted(), 1);
|
||||
Network::to_pretty_bytes(data.total_transmitted(), 1);
|
||||
|
||||
total_activity.push(NetworkReading::new(
|
||||
NetworkReadingFormat::Total,
|
||||
ReadingValue::from(total_received),
|
||||
ReadingValue::from(total_transmitted),
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_state_activity.clone_from(&activity);
|
||||
self.last_state_total_activity.clone_from(&total_activity);
|
||||
self.last_updated_network_activity = now;
|
||||
// Only update if this is the latest request
|
||||
if gen_ == gen_arc.load(Ordering::SeqCst) {
|
||||
if let Ok(mut act) = activity_arc.lock() {
|
||||
*act = activity;
|
||||
}
|
||||
if let Ok(mut tot) = total_activity_arc.lock() {
|
||||
*tot = total_activity;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let now = Instant::now();
|
||||
let should_update = {
|
||||
let last_update_request = self.last_update_request_network_activity.lock().unwrap();
|
||||
now.duration_since(*last_update_request)
|
||||
> Duration::from_secs(self.data_refresh_interval)
|
||||
};
|
||||
|
||||
if should_update {
|
||||
{
|
||||
let mut last_updated = self.last_update_request_network_activity.lock().unwrap();
|
||||
*last_updated = now;
|
||||
}
|
||||
self.update_network_activity_async();
|
||||
}
|
||||
|
||||
self.get_network_activity()
|
||||
}
|
||||
|
||||
fn get_network_activity(&self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
|
||||
let activity = self.last_state_activity.lock().unwrap().clone();
|
||||
let total_activity = self.last_state_total_activity.lock().unwrap().clone();
|
||||
(activity, total_activity)
|
||||
}
|
||||
|
||||
@@ -445,9 +507,9 @@ impl BarWidget for Network {
|
||||
}
|
||||
|
||||
if self.show_default_interface {
|
||||
self.default_interface();
|
||||
let mut self_default_interface = self.default_interface();
|
||||
|
||||
if !self.default_interface.is_empty() {
|
||||
if !self_default_interface.is_empty() {
|
||||
let mut layout_job = LayoutJob::simple(
|
||||
match self.label_prefix {
|
||||
LabelPrefix::Icon | LabelPrefix::IconAndText => {
|
||||
@@ -461,11 +523,11 @@ impl BarWidget for Network {
|
||||
);
|
||||
|
||||
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
|
||||
self.default_interface.insert_str(0, "NET: ");
|
||||
self_default_interface.insert_str(0, "NET: ");
|
||||
}
|
||||
|
||||
layout_job.append(
|
||||
&self.default_interface,
|
||||
&self_default_interface,
|
||||
10.0,
|
||||
TextFormat {
|
||||
font_id: config.text_font_id.clone(),
|
||||
|
||||
@@ -18,16 +18,20 @@ use sysinfo::Disks;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Storage widget configuration
|
||||
pub struct StorageConfig {
|
||||
/// Enable the Storage widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 10 seconds)
|
||||
/// Data refresh interval in seconds
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Show disks that are read only. (default: false)
|
||||
/// Show disks that are read only
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
|
||||
pub show_read_only_disks: Option<bool>,
|
||||
/// Show removable disks. (default: true)
|
||||
/// 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>,
|
||||
|
||||
@@ -72,6 +72,7 @@ lazy_static! {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Time widget configuration
|
||||
pub struct TimeConfig {
|
||||
/// Enable the Time widget
|
||||
pub enable: bool,
|
||||
@@ -92,7 +93,7 @@ pub struct TimeConfig {
|
||||
///}
|
||||
/// ```
|
||||
pub timezone: Option<String>,
|
||||
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00. (default: false)
|
||||
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00
|
||||
pub changing_icon: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -119,6 +120,7 @@ impl From<TimeConfig> for Time {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Time format
|
||||
pub enum TimeFormat {
|
||||
/// Twelve-hour format (with seconds)
|
||||
TwelveHour,
|
||||
@@ -133,6 +135,7 @@ pub enum TimeFormat {
|
||||
/// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
|
||||
BinaryRectangle,
|
||||
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ use std::time::Instant;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Update widget configuration
|
||||
pub struct UpdateConfig {
|
||||
/// Enable the Update widget
|
||||
pub enable: bool,
|
||||
/// Data refresh interval (default: 12 hours)
|
||||
/// Data refresh interval in hours
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12)))]
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
|
||||
@@ -34,18 +34,43 @@ 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),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.39"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -7,21 +7,26 @@ pub use komorebi::AspectRatio;
|
||||
pub use komorebi::BorderColours;
|
||||
pub use komorebi::Colour;
|
||||
pub use komorebi::CrossBoundaryBehaviour;
|
||||
pub use komorebi::GlobalState;
|
||||
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::State;
|
||||
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;
|
||||
@@ -68,6 +73,9 @@ 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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.39"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -13,4 +13,4 @@ egui_extras = { workspace = true }
|
||||
random_word = { version = "0.5", features = ["en"] }
|
||||
serde_json = { workspace = true }
|
||||
windows-core = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
windows = { workspace = true }
|
||||
|
||||
@@ -247,7 +247,7 @@ impl eframe::App for KomorebiGui {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ctx.set_pixels_per_point(2.0);
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.set_width(ctx.screen_rect().width());
|
||||
ui.set_width(ctx.content_rect().width());
|
||||
ui.collapsing("Debugging", |ui| {
|
||||
ui.collapsing("Window Rules", |ui| {
|
||||
let window = Window::from(self.debug_hwnd);
|
||||
|
||||
26
komorebi-layouts/Cargo.toml
Normal file
26
komorebi-layouts/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "komorebi-layouts"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# Optional dependencies
|
||||
schemars = { workspace = true, optional = true }
|
||||
windows = { workspace = true, optional = true }
|
||||
objc2-core-foundation = { version = "0.3", default-features = false, features = [
|
||||
"std",
|
||||
"CFCGTypes",
|
||||
], optional = true }
|
||||
|
||||
[features]
|
||||
schemars = ["dep:schemars"]
|
||||
win32 = ["dep:windows"]
|
||||
darwin = ["dep:objc2-core-foundation"]
|
||||
File diff suppressed because it is too large
Load Diff
785
komorebi-layouts/src/default_layout.rs
Normal file
785
komorebi-layouts/src/default_layout.rs
Normal file
@@ -0,0 +1,785 @@
|
||||
use clap::ValueEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use super::OperationDirection;
|
||||
use super::Rect;
|
||||
use super::Sizing;
|
||||
|
||||
/// Maximum number of ratio values that can be specified for column_ratios and row_ratios
|
||||
pub const MAX_RATIOS: usize = 5;
|
||||
|
||||
/// Minimum allowed ratio value (prevents zero-sized windows)
|
||||
pub const MIN_RATIO: f32 = 0.1;
|
||||
|
||||
/// Maximum allowed ratio value (ensures space for remaining windows)
|
||||
pub const MAX_RATIO: f32 = 0.9;
|
||||
|
||||
/// Default ratio value when none is specified
|
||||
pub const DEFAULT_RATIO: f32 = 0.5;
|
||||
|
||||
/// Default secondary ratio value for UltrawideVerticalStack layout
|
||||
pub const DEFAULT_SECONDARY_RATIO: f32 = 0.25;
|
||||
|
||||
/// Validates and converts a Vec of ratios into a fixed-size array.
|
||||
/// - Clamps values to MIN_RATIO..MAX_RATIO range
|
||||
/// - Truncates when cumulative sum reaches or exceeds 1.0
|
||||
/// - Limits to MAX_RATIOS values
|
||||
#[must_use]
|
||||
pub fn validate_ratios(ratios: &[f32]) -> [Option<f32>; MAX_RATIOS] {
|
||||
let mut arr = [None; MAX_RATIOS];
|
||||
let mut cumulative_sum = 0.0_f32;
|
||||
|
||||
for (i, &val) in ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||
let clamped_val = val.clamp(MIN_RATIO, MAX_RATIO);
|
||||
|
||||
// Only add this ratio if cumulative sum stays below 1.0
|
||||
if cumulative_sum + clamped_val < 1.0 {
|
||||
arr[i] = Some(clamped_val);
|
||||
cumulative_sum += clamped_val;
|
||||
} else {
|
||||
// Stop adding ratios - cumulative sum would reach or exceed 1.0
|
||||
tracing::debug!(
|
||||
"Truncating ratios at index {} - cumulative sum {} + {} would reach/exceed 1.0",
|
||||
i,
|
||||
cumulative_sum,
|
||||
clamped_val
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
arr
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// A predefined komorebi layout
|
||||
pub enum DefaultLayout {
|
||||
/// BSP Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-------+-----+
|
||||
/// | | |
|
||||
/// | +--+--+
|
||||
/// | | |--|
|
||||
/// +-------+--+--+
|
||||
/// ```
|
||||
BSP,
|
||||
/// Columns Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +--+--+--+--+
|
||||
/// | | | | |
|
||||
/// | | | | |
|
||||
/// | | | | |
|
||||
/// +--+--+--+--+
|
||||
/// ```
|
||||
Columns,
|
||||
/// Rows Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-----------+
|
||||
/// |-----------|
|
||||
/// |-----------|
|
||||
/// |-----------|
|
||||
/// +-----------+
|
||||
/// ```
|
||||
Rows,
|
||||
/// Vertical Stack Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-------+-----+
|
||||
/// | | |
|
||||
/// | +-----+
|
||||
/// | | |
|
||||
/// +-------+-----+
|
||||
/// ```
|
||||
VerticalStack,
|
||||
/// Horizontal Stack Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +------+------+
|
||||
/// | |
|
||||
/// |------+------+
|
||||
/// | | |
|
||||
/// +------+------+
|
||||
/// ```
|
||||
HorizontalStack,
|
||||
/// Ultrawide Vertical Stack Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-----+-----------+-----+
|
||||
/// | | | |
|
||||
/// | | +-----+
|
||||
/// | | | |
|
||||
/// | | +-----+
|
||||
/// | | | |
|
||||
/// +-----+-----------+-----+
|
||||
/// ```
|
||||
UltrawideVerticalStack,
|
||||
/// Grid Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||
/// | | | | | | | | | | | | | | |
|
||||
/// | | | | | | | | | | | | | +---+
|
||||
/// +-----+-----+ | +---+---+ +---+---+---+ +---+---| |
|
||||
/// | | | | | | | | | | | | | +---+
|
||||
/// | | | | | | | | | | | | | | |
|
||||
/// +-----+-----+ +---+---+---+ +---+---+---+ +---+---+---+
|
||||
/// 4 windows 5 windows 6 windows 7 windows
|
||||
/// ```
|
||||
Grid,
|
||||
/// Right Main Vertical Stack Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +-----+-------+
|
||||
/// | | |
|
||||
/// +-----+ |
|
||||
/// | | |
|
||||
/// +-----+-------+
|
||||
/// ```
|
||||
RightMainVerticalStack,
|
||||
/// Scrolling Layout
|
||||
///
|
||||
/// ```text
|
||||
/// +--+--+--+--+--+--+
|
||||
/// | | | |
|
||||
/// | | | |
|
||||
/// | | | |
|
||||
/// +--+--+--+--+--+--+
|
||||
/// ```
|
||||
Scrolling,
|
||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
||||
}
|
||||
|
||||
/// Helper to deserialize a variable-length array into a fixed [Option<f32>; MAX_RATIOS]
|
||||
/// Ratios are truncated when their cumulative sum reaches or exceeds 1.0 to ensure
|
||||
/// there's always remaining space for additional windows.
|
||||
fn deserialize_ratios<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<[Option<f32>; MAX_RATIOS]>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let opt: Option<Vec<f32>> = Option::deserialize(deserializer)?;
|
||||
Ok(opt.map(|vec| validate_ratios(&vec)))
|
||||
}
|
||||
|
||||
/// Helper to serialize [Option<f32>; MAX_RATIOS] as a compact array (without trailing nulls)
|
||||
fn serialize_ratios<S>(
|
||||
value: &Option<[Option<f32>; MAX_RATIOS]>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match value {
|
||||
None => serializer.serialize_none(),
|
||||
Some(arr) => {
|
||||
// Find last non-None index
|
||||
let last_idx = arr
|
||||
.iter()
|
||||
.rposition(|x| x.is_some())
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0);
|
||||
let vec: Vec<f32> = arr.iter().take(last_idx).filter_map(|&x| x).collect();
|
||||
serializer.serialize_some(&vec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Options for specific layouts
|
||||
pub struct LayoutOptions {
|
||||
/// Options related to the Scrolling layout
|
||||
pub scrolling: Option<ScrollingLayoutOptions>,
|
||||
/// Options related to the Grid layout
|
||||
pub grid: Option<GridLayoutOptions>,
|
||||
/// Column width ratios (up to MAX_RATIOS values between 0.1 and 0.9)
|
||||
///
|
||||
/// - Used by Columns layout: ratios for each column width
|
||||
/// - Used by Grid layout: ratios for column widths
|
||||
/// - Used by BSP, VerticalStack, RightMainVerticalStack: column_ratios[0] as primary split ratio
|
||||
/// - Used by HorizontalStack: column_ratios[0] as primary split ratio (top area height)
|
||||
/// - Used by UltrawideVerticalStack: column_ratios[0] as center ratio, column_ratios[1] as left ratio
|
||||
///
|
||||
/// Columns without a ratio share remaining space equally.
|
||||
/// Example: `[0.3, 0.4, 0.3]` for 30%-40%-30% columns
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_ratios",
|
||||
serialize_with = "serialize_ratios"
|
||||
)]
|
||||
pub column_ratios: Option<[Option<f32>; MAX_RATIOS]>,
|
||||
/// Row height ratios (up to MAX_RATIOS values between 0.1 and 0.9)
|
||||
///
|
||||
/// - Used by Rows layout: ratios for each row height
|
||||
/// - Used by Grid layout: ratios for row heights
|
||||
///
|
||||
/// Rows without a ratio share remaining space equally.
|
||||
/// Example: `[0.5, 0.5]` for 50%-50% rows
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_ratios",
|
||||
serialize_with = "serialize_ratios"
|
||||
)]
|
||||
pub row_ratios: Option<[Option<f32>; MAX_RATIOS]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Options for the Scrolling layout
|
||||
pub struct ScrollingLayoutOptions {
|
||||
/// Desired number of visible columns (default: 3)
|
||||
pub columns: usize,
|
||||
/// With an odd number of visible columns, keep the focused window column centered
|
||||
pub center_focused_column: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Options for the Grid layout
|
||||
pub struct GridLayoutOptions {
|
||||
/// Maximum number of rows per grid column
|
||||
pub rows: usize,
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
Self::UltrawideVerticalStack | Self::RightMainVerticalStack => match len {
|
||||
n if n > 1 => 1,
|
||||
_ => 0,
|
||||
},
|
||||
Self::Scrolling => 0,
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::HorizontalStack
|
||||
| DefaultLayout::Grid => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rightmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::HorizontalStack
|
||||
| DefaultLayout::Grid => len.saturating_sub(1),
|
||||
DefaultLayout::UltrawideVerticalStack => match len {
|
||||
2 => 0,
|
||||
_ => len.saturating_sub(1),
|
||||
},
|
||||
DefaultLayout::RightMainVerticalStack => 0,
|
||||
DefaultLayout::Scrolling => len.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
|
||||
pub fn resize(
|
||||
&self,
|
||||
unaltered: &Rect,
|
||||
resize: &Option<Rect>,
|
||||
edge: OperationDirection,
|
||||
sizing: Sizing,
|
||||
delta: i32,
|
||||
) -> Option<Rect> {
|
||||
if !matches!(
|
||||
self,
|
||||
Self::BSP
|
||||
| Self::Columns
|
||||
| Self::Rows
|
||||
| Self::VerticalStack
|
||||
| Self::RightMainVerticalStack
|
||||
| Self::HorizontalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::Scrolling
|
||||
) {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut r = resize.unwrap_or_default();
|
||||
|
||||
let resize_delta = delta;
|
||||
|
||||
match edge {
|
||||
OperationDirection::Left => match sizing {
|
||||
Sizing::Increase => {
|
||||
// Some final checks to make sure the user can't infinitely resize to
|
||||
// the point of pushing other windows out of bounds
|
||||
|
||||
// Note: These checks cannot take into account the changes made to the
|
||||
// edges of adjacent windows at operation time, so it is still possible
|
||||
// to push windows out of bounds by maxing out an Increase Left on a
|
||||
// Window with index 1, and then maxing out a Decrease Right on a Window
|
||||
// with index 0. I don't think it's worth trying to defensively program
|
||||
// against this; if people end up in this situation they are better off
|
||||
// just hitting the retile command
|
||||
let diff = ((r.left + -resize_delta) as f32).abs();
|
||||
if diff < unaltered.right as f32 {
|
||||
r.left += -resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.left - -resize_delta) as f32).abs();
|
||||
if diff < unaltered.right as f32 {
|
||||
r.left -= -resize_delta;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Up => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.top + resize_delta) as f32).abs();
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.top += -resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.top - resize_delta) as f32).abs();
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.top -= -resize_delta;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Right => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.right + resize_delta) as f32).abs();
|
||||
if diff < unaltered.right as f32 {
|
||||
r.right += resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.right - resize_delta) as f32).abs();
|
||||
if diff < unaltered.right as f32 {
|
||||
r.right -= resize_delta;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Down => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.bottom + resize_delta) as f32).abs();
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.bottom += resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.bottom - resize_delta) as f32).abs();
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.bottom -= resize_delta;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if r.eq(&Rect::default()) {
|
||||
None
|
||||
} else {
|
||||
Option::from(r)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn cycle_next(self) -> Self {
|
||||
match self {
|
||||
Self::BSP => Self::Columns,
|
||||
Self::Columns => Self::Rows,
|
||||
Self::Rows => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Scrolling,
|
||||
Self::Scrolling => Self::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn cycle_previous(self) -> Self {
|
||||
match self {
|
||||
Self::Scrolling => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::Rows,
|
||||
Self::Rows => Self::Columns,
|
||||
Self::Columns => Self::BSP,
|
||||
Self::BSP => Self::RightMainVerticalStack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Helper to create LayoutOptions with column ratios
|
||||
fn layout_options_with_column_ratios(ratios: &[f32]) -> LayoutOptions {
|
||||
let mut arr = [None; MAX_RATIOS];
|
||||
for (i, &r) in ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||
arr[i] = Some(r);
|
||||
}
|
||||
LayoutOptions {
|
||||
scrolling: None,
|
||||
grid: None,
|
||||
column_ratios: Some(arr),
|
||||
row_ratios: None,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create LayoutOptions with row ratios
|
||||
fn layout_options_with_row_ratios(ratios: &[f32]) -> LayoutOptions {
|
||||
let mut arr = [None; MAX_RATIOS];
|
||||
for (i, &r) in ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||
arr[i] = Some(r);
|
||||
}
|
||||
LayoutOptions {
|
||||
scrolling: None,
|
||||
grid: None,
|
||||
column_ratios: None,
|
||||
row_ratios: Some(arr),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create LayoutOptions with both column and row ratios
|
||||
fn layout_options_with_ratios(column_ratios: &[f32], row_ratios: &[f32]) -> LayoutOptions {
|
||||
let mut col_arr = [None; MAX_RATIOS];
|
||||
for (i, &r) in column_ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||
col_arr[i] = Some(r);
|
||||
}
|
||||
let mut row_arr = [None; MAX_RATIOS];
|
||||
for (i, &r) in row_ratios.iter().take(MAX_RATIOS).enumerate() {
|
||||
row_arr[i] = Some(r);
|
||||
}
|
||||
LayoutOptions {
|
||||
scrolling: None,
|
||||
grid: None,
|
||||
column_ratios: Some(col_arr),
|
||||
row_ratios: Some(row_arr),
|
||||
}
|
||||
}
|
||||
|
||||
mod deserialize_ratios_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_valid_ratios() {
|
||||
let json = r#"{"column_ratios": [0.3, 0.4, 0.2]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(0.3));
|
||||
assert_eq!(ratios[1], Some(0.4));
|
||||
assert_eq!(ratios[2], Some(0.2));
|
||||
assert_eq!(ratios[3], None);
|
||||
assert_eq!(ratios[4], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_clamps_values_to_min() {
|
||||
// Values below MIN_RATIO should be clamped
|
||||
let json = r#"{"column_ratios": [0.05]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(MIN_RATIO)); // Clamped to 0.1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_clamps_values_to_max() {
|
||||
// Values above MAX_RATIO should be clamped
|
||||
let json = r#"{"column_ratios": [0.95]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
// 0.9 is the max, so it should be clamped
|
||||
assert!(ratios[0].unwrap() <= MAX_RATIO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_truncates_when_sum_exceeds_one() {
|
||||
// Sum of ratios should not reach 1.0
|
||||
// [0.5, 0.4] = 0.9, then 0.3 would make it 1.2, so it should be truncated
|
||||
let json = r#"{"column_ratios": [0.5, 0.4, 0.3]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(0.5));
|
||||
assert_eq!(ratios[1], Some(0.4));
|
||||
// Third ratio should be truncated because 0.5 + 0.4 + 0.3 >= 1.0
|
||||
assert_eq!(ratios[2], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_truncates_at_max_ratios() {
|
||||
// More than MAX_RATIOS values should be truncated
|
||||
let json = r#"{"column_ratios": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
// Only MAX_RATIOS (5) values should be stored
|
||||
for i in 0..MAX_RATIOS {
|
||||
assert_eq!(ratios[i], Some(0.1));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_empty_array() {
|
||||
let json = r#"{"column_ratios": []}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.column_ratios.unwrap();
|
||||
for i in 0..MAX_RATIOS {
|
||||
assert_eq!(ratios[i], None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_null() {
|
||||
let json = r#"{"column_ratios": null}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
assert!(opts.column_ratios.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_row_ratios() {
|
||||
let json = r#"{"row_ratios": [0.3, 0.5]}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
let ratios = opts.row_ratios.unwrap();
|
||||
assert_eq!(ratios[0], Some(0.3));
|
||||
assert_eq!(ratios[1], Some(0.5));
|
||||
assert_eq!(ratios[2], None);
|
||||
}
|
||||
}
|
||||
|
||||
mod serialize_ratios_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialize_ratios_compact() {
|
||||
let opts = layout_options_with_column_ratios(&[0.3, 0.4]);
|
||||
let json = serde_json::to_string(&opts).unwrap();
|
||||
|
||||
// Should serialize ratios as compact array without trailing nulls in the ratios array
|
||||
assert!(json.contains("0.3") && json.contains("0.4"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_none_ratios() {
|
||||
let opts = LayoutOptions {
|
||||
scrolling: None,
|
||||
grid: None,
|
||||
column_ratios: None,
|
||||
row_ratios: None,
|
||||
};
|
||||
let json = serde_json::to_string(&opts).unwrap();
|
||||
|
||||
// None values should serialize as null or be omitted
|
||||
assert!(!json.contains("["));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_serialization() {
|
||||
let original = layout_options_with_column_ratios(&[0.3, 0.4, 0.2]);
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(original.column_ratios, deserialized.column_ratios);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_row_ratios() {
|
||||
let opts = layout_options_with_row_ratios(&[0.3, 0.5]);
|
||||
let json = serde_json::to_string(&opts).unwrap();
|
||||
|
||||
assert!(json.contains("row_ratios"));
|
||||
assert!(json.contains("0.3") && json.contains("0.5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_row_ratios() {
|
||||
let original = layout_options_with_row_ratios(&[0.4, 0.3]);
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(original.row_ratios, deserialized.row_ratios);
|
||||
assert!(original.column_ratios.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_both_ratios() {
|
||||
let original = layout_options_with_ratios(&[0.3, 0.4], &[0.5, 0.3]);
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: LayoutOptions = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(original.column_ratios, deserialized.column_ratios);
|
||||
assert_eq!(original.row_ratios, deserialized.row_ratios);
|
||||
}
|
||||
}
|
||||
|
||||
mod ratio_constants_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_constants_valid_ranges() {
|
||||
assert!(MIN_RATIO > 0.0);
|
||||
assert!(MIN_RATIO < MAX_RATIO);
|
||||
assert!(MAX_RATIO < 1.0);
|
||||
assert!(DEFAULT_RATIO >= MIN_RATIO && DEFAULT_RATIO <= MAX_RATIO);
|
||||
assert!(DEFAULT_SECONDARY_RATIO >= MIN_RATIO && DEFAULT_SECONDARY_RATIO <= MAX_RATIO);
|
||||
assert!(MAX_RATIOS >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_ratio_is_half() {
|
||||
assert_eq!(DEFAULT_RATIO, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_ratios_is_five() {
|
||||
assert_eq!(MAX_RATIOS, 5);
|
||||
}
|
||||
}
|
||||
|
||||
mod layout_options_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_layout_options_default_values() {
|
||||
let json = r#"{}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert!(opts.scrolling.is_none());
|
||||
assert!(opts.grid.is_none());
|
||||
assert!(opts.column_ratios.is_none());
|
||||
assert!(opts.row_ratios.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_options_with_all_fields() {
|
||||
let json = r#"{
|
||||
"scrolling": {"columns": 3},
|
||||
"grid": {"rows": 2},
|
||||
"column_ratios": [0.3, 0.4],
|
||||
"row_ratios": [0.5]
|
||||
}"#;
|
||||
let opts: LayoutOptions = serde_json::from_str(json).unwrap();
|
||||
|
||||
assert!(opts.scrolling.is_some());
|
||||
assert_eq!(opts.scrolling.unwrap().columns, 3);
|
||||
assert!(opts.grid.is_some());
|
||||
assert_eq!(opts.grid.unwrap().rows, 2);
|
||||
assert!(opts.column_ratios.is_some());
|
||||
assert!(opts.row_ratios.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
mod default_layout_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cycle_next_covers_all_layouts() {
|
||||
let start = DefaultLayout::BSP;
|
||||
let mut current = start;
|
||||
let mut visited = vec![current];
|
||||
|
||||
loop {
|
||||
current = current.cycle_next();
|
||||
if current == start {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
!visited.contains(¤t),
|
||||
"Cycle contains duplicate: {:?}",
|
||||
current
|
||||
);
|
||||
visited.push(current);
|
||||
}
|
||||
|
||||
// Should have visited all layouts
|
||||
assert_eq!(visited.len(), 9); // 9 layouts total
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_previous_is_inverse_of_next() {
|
||||
// Note: cycle_previous has some inconsistencies in the current implementation
|
||||
// This test documents the expected behavior for most layouts
|
||||
let layouts_with_correct_inverse = [
|
||||
DefaultLayout::Columns,
|
||||
DefaultLayout::Rows,
|
||||
DefaultLayout::VerticalStack,
|
||||
DefaultLayout::HorizontalStack,
|
||||
DefaultLayout::UltrawideVerticalStack,
|
||||
DefaultLayout::Grid,
|
||||
DefaultLayout::RightMainVerticalStack,
|
||||
];
|
||||
|
||||
for layout in layouts_with_correct_inverse {
|
||||
let next = layout.cycle_next();
|
||||
assert_eq!(
|
||||
next.cycle_previous(),
|
||||
layout,
|
||||
"cycle_previous should be inverse of cycle_next for {:?}",
|
||||
layout
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leftmost_index_standard_layouts() {
|
||||
assert_eq!(DefaultLayout::BSP.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::Columns.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::Rows.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::VerticalStack.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::HorizontalStack.leftmost_index(5), 0);
|
||||
assert_eq!(DefaultLayout::Grid.leftmost_index(5), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leftmost_index_ultrawide() {
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(2), 1);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.leftmost_index(5), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leftmost_index_right_main() {
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(2), 1);
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.leftmost_index(5), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rightmost_index_standard_layouts() {
|
||||
assert_eq!(DefaultLayout::BSP.rightmost_index(5), 4);
|
||||
assert_eq!(DefaultLayout::Columns.rightmost_index(5), 4);
|
||||
assert_eq!(DefaultLayout::Rows.rightmost_index(5), 4);
|
||||
assert_eq!(DefaultLayout::VerticalStack.rightmost_index(5), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rightmost_index_right_main() {
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.rightmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::RightMainVerticalStack.rightmost_index(5), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rightmost_index_ultrawide() {
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(1), 0);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(2), 0);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(3), 2);
|
||||
assert_eq!(DefaultLayout::UltrawideVerticalStack.rightmost_index(5), 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
use super::DefaultLayout;
|
||||
use super::OperationDirection;
|
||||
#[cfg(feature = "win32")]
|
||||
use super::custom_layout::Column;
|
||||
#[cfg(feature = "win32")]
|
||||
use super::custom_layout::ColumnSplit;
|
||||
#[cfg(feature = "win32")]
|
||||
use super::custom_layout::ColumnSplitWithCapacity;
|
||||
#[cfg(feature = "win32")]
|
||||
use super::custom_layout::CustomLayout;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
|
||||
pub trait Direction {
|
||||
fn index_in_direction(
|
||||
@@ -11,6 +16,7 @@ pub trait Direction {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize>;
|
||||
|
||||
fn is_valid_direction(
|
||||
@@ -18,30 +24,35 @@ pub trait Direction {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool;
|
||||
fn up_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
fn down_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
fn left_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
fn right_index(
|
||||
&self,
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize;
|
||||
}
|
||||
|
||||
@@ -51,32 +62,53 @@ impl Direction for DefaultLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize> {
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.left_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.right_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.up_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(Some(op_direction), idx, Some(count)))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.down_index(
|
||||
Some(op_direction),
|
||||
idx,
|
||||
Some(count),
|
||||
layout_options,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -89,6 +121,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool {
|
||||
if count < 2 {
|
||||
return false;
|
||||
@@ -101,7 +134,7 @@ impl Direction for DefaultLayout {
|
||||
Self::Rows | Self::HorizontalStack => idx != 0,
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx > 2,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Down => match self {
|
||||
@@ -111,7 +144,7 @@ impl Direction for DefaultLayout {
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
|
||||
Self::HorizontalStack => idx == 0,
|
||||
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
@@ -121,7 +154,7 @@ impl Direction for DefaultLayout {
|
||||
Self::Rows => false,
|
||||
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx != 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => idx != 0,
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
@@ -135,7 +168,7 @@ impl Direction for DefaultLayout {
|
||||
2 => idx != 0,
|
||||
_ => idx < 2,
|
||||
},
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => idx != count - 1,
|
||||
},
|
||||
}
|
||||
@@ -146,6 +179,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
@@ -161,7 +195,7 @@ impl Direction for DefaultLayout {
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::RightMainVerticalStack => idx - 1,
|
||||
Self::HorizontalStack => 0,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -171,6 +205,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP
|
||||
@@ -180,7 +215,7 @@ impl Direction for DefaultLayout {
|
||||
| Self::RightMainVerticalStack => idx + 1,
|
||||
Self::Columns => unreachable!(),
|
||||
Self::HorizontalStack => 1,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -190,6 +225,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP => {
|
||||
@@ -208,7 +244,7 @@ impl Direction for DefaultLayout {
|
||||
1 => unreachable!(),
|
||||
_ => 0,
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => idx - 1,
|
||||
}
|
||||
}
|
||||
@@ -218,6 +254,7 @@ impl Direction for DefaultLayout {
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
match self {
|
||||
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
|
||||
@@ -229,7 +266,7 @@ impl Direction for DefaultLayout {
|
||||
0 => 2,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count, layout_options),
|
||||
Self::Scrolling => idx + 1,
|
||||
}
|
||||
}
|
||||
@@ -260,21 +297,32 @@ struct GridTouchingEdges {
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn get_grid_item(idx: usize, count: usize) -> GridItem {
|
||||
let num_cols = (count as f32).sqrt().ceil() as usize;
|
||||
fn get_grid_item(idx: usize, count: usize, layout_options: Option<LayoutOptions>) -> GridItem {
|
||||
let row_constraint = layout_options.and_then(|o| o.grid.map(|g| g.rows));
|
||||
let num_cols = if let Some(rows) = row_constraint {
|
||||
((count as f32) / (rows as f32)).ceil() as i32
|
||||
} else {
|
||||
(count as f32).sqrt().ceil() as i32
|
||||
};
|
||||
|
||||
let mut iter = 0;
|
||||
|
||||
for col in 0..num_cols {
|
||||
let remaining_windows = count - iter;
|
||||
let remaining_windows = (count - iter) as i32;
|
||||
let remaining_columns = num_cols - col;
|
||||
let num_rows_in_this_col = remaining_windows / remaining_columns;
|
||||
|
||||
let num_rows_in_this_col = if let Some(rows) = row_constraint {
|
||||
(remaining_windows / remaining_columns).min(rows as i32)
|
||||
} else {
|
||||
remaining_windows / remaining_columns
|
||||
};
|
||||
|
||||
for row in 0..num_rows_in_this_col {
|
||||
if iter == idx {
|
||||
return GridItem {
|
||||
state: GridItemState::Valid,
|
||||
row: row + 1,
|
||||
num_rows: num_rows_in_this_col,
|
||||
row: (row + 1) as usize,
|
||||
num_rows: num_rows_in_this_col as usize,
|
||||
touching_edges: GridTouchingEdges {
|
||||
left: col == 0,
|
||||
right: col == num_cols - 1,
|
||||
@@ -301,8 +349,13 @@ fn get_grid_item(idx: usize, count: usize) -> GridItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_grid_edge(op_direction: OperationDirection, idx: usize, count: usize) -> bool {
|
||||
let item = get_grid_item(idx, count);
|
||||
fn is_grid_edge(
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool {
|
||||
let item = get_grid_item(idx, count, layout_options);
|
||||
|
||||
match item.state {
|
||||
GridItemState::Invalid => false,
|
||||
@@ -319,6 +372,7 @@ fn grid_neighbor(
|
||||
op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
count: Option<usize>,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
let Some(op_direction) = op_direction else {
|
||||
return 0;
|
||||
@@ -328,11 +382,11 @@ fn grid_neighbor(
|
||||
return 0;
|
||||
};
|
||||
|
||||
let item = get_grid_item(idx, count);
|
||||
let item = get_grid_item(idx, count, layout_options);
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
let item_from_prev_col = get_grid_item(idx - item.row, count);
|
||||
let item_from_prev_col = get_grid_item(idx - item.row, count, layout_options);
|
||||
|
||||
if item.touching_edges.up && item.num_rows != item_from_prev_col.num_rows {
|
||||
return idx - (item.num_rows - 1);
|
||||
@@ -350,42 +404,49 @@ fn grid_neighbor(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "win32")]
|
||||
impl Direction for CustomLayout {
|
||||
fn index_in_direction(
|
||||
&self,
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize> {
|
||||
if count <= self.len() {
|
||||
return DefaultLayout::Columns.index_in_direction(op_direction, idx, count);
|
||||
return DefaultLayout::Columns.index_in_direction(
|
||||
op_direction,
|
||||
idx,
|
||||
count,
|
||||
layout_options,
|
||||
);
|
||||
}
|
||||
|
||||
match op_direction {
|
||||
OperationDirection::Left => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.left_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.left_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Right => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.right_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.right_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Up => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.up_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.up_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OperationDirection::Down => {
|
||||
if self.is_valid_direction(op_direction, idx, count) {
|
||||
Option::from(self.down_index(None, idx, None))
|
||||
if self.is_valid_direction(op_direction, idx, count, layout_options) {
|
||||
Option::from(self.down_index(None, idx, None, layout_options))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -398,9 +459,15 @@ impl Direction for CustomLayout {
|
||||
op_direction: OperationDirection,
|
||||
idx: usize,
|
||||
count: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> bool {
|
||||
if count <= self.len() {
|
||||
return DefaultLayout::Columns.is_valid_direction(op_direction, idx, count);
|
||||
return DefaultLayout::Columns.is_valid_direction(
|
||||
op_direction,
|
||||
idx,
|
||||
count,
|
||||
layout_options,
|
||||
);
|
||||
}
|
||||
|
||||
match op_direction {
|
||||
@@ -444,6 +511,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
idx - 1
|
||||
}
|
||||
@@ -453,6 +521,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
idx + 1
|
||||
}
|
||||
@@ -462,6 +531,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
if column_idx - 1 == 0 {
|
||||
@@ -476,6 +546,7 @@ impl Direction for CustomLayout {
|
||||
_op_direction: Option<OperationDirection>,
|
||||
idx: usize,
|
||||
_count: Option<usize>,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
) -> usize {
|
||||
let column_idx = self.column_for_container_idx(idx);
|
||||
self.first_container_idx(column_idx + 1)
|
||||
@@ -2,6 +2,7 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::Arrangement;
|
||||
#[cfg(feature = "win32")]
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Direction;
|
||||
@@ -10,6 +11,7 @@ use super::Direction;
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum Layout {
|
||||
Default(DefaultLayout),
|
||||
#[cfg(feature = "win32")]
|
||||
Custom(CustomLayout),
|
||||
}
|
||||
|
||||
@@ -18,6 +20,7 @@ impl Layout {
|
||||
pub fn as_boxed_direction(&self) -> Box<dyn Direction> {
|
||||
match self {
|
||||
Layout::Default(layout) => Box::new(*layout),
|
||||
#[cfg(feature = "win32")]
|
||||
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||
}
|
||||
}
|
||||
@@ -26,6 +29,7 @@ impl Layout {
|
||||
pub fn as_boxed_arrangement(&self) -> Box<dyn Arrangement> {
|
||||
match self {
|
||||
Layout::Default(layout) => Box::new(*layout),
|
||||
#[cfg(feature = "win32")]
|
||||
Layout::Custom(layout) => Box::new(layout.clone()),
|
||||
}
|
||||
}
|
||||
30
komorebi-layouts/src/lib.rs
Normal file
30
komorebi-layouts/src/lib.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||
|
||||
//! Layout system for the komorebi window manager.
|
||||
//!
|
||||
//! This crate provides the core layout algorithms and types for arranging windows
|
||||
//! in various configurations. It includes optional Windows-specific functionality
|
||||
//! behind the `win32` feature flag.
|
||||
|
||||
pub mod arrangement;
|
||||
#[cfg(feature = "win32")]
|
||||
pub mod custom_layout;
|
||||
pub mod cycle_direction;
|
||||
pub mod default_layout;
|
||||
pub mod direction;
|
||||
pub mod layout;
|
||||
pub mod operation_direction;
|
||||
pub mod rect;
|
||||
pub mod sizing;
|
||||
|
||||
pub use arrangement::*;
|
||||
#[cfg(feature = "win32")]
|
||||
pub use custom_layout::*;
|
||||
pub use cycle_direction::*;
|
||||
pub use default_layout::*;
|
||||
pub use direction::*;
|
||||
pub use layout::*;
|
||||
pub use operation_direction::*;
|
||||
pub use rect::*;
|
||||
pub use sizing::*;
|
||||
@@ -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::Axis;
|
||||
use super::direction::Direction;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum OperationDirection {
|
||||
@@ -57,7 +57,8 @@ impl OperationDirection {
|
||||
layout_flip: Option<Axis>,
|
||||
idx: usize,
|
||||
len: NonZeroUsize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
) -> Option<usize> {
|
||||
layout.index_in_direction(self.flip(layout_flip), idx, len.get())
|
||||
layout.index_in_direction(self.flip(layout_flip), idx, len.get(), layout_options)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,33 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[cfg(feature = "win32")]
|
||||
use windows::Win32::Foundation::RECT;
|
||||
|
||||
#[cfg(feature = "darwin")]
|
||||
use objc2_core_foundation::CGFloat;
|
||||
#[cfg(feature = "darwin")]
|
||||
use objc2_core_foundation::CGPoint;
|
||||
#[cfg(feature = "darwin")]
|
||||
use objc2_core_foundation::CGRect;
|
||||
#[cfg(feature = "darwin")]
|
||||
use objc2_core_foundation::CGSize;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Rectangle dimensions
|
||||
pub struct Rect {
|
||||
/// The left point in a Win32 Rect
|
||||
/// Left point of the rectangle
|
||||
pub left: i32,
|
||||
/// The top point in a Win32 Rect
|
||||
/// Top point of the rectangle
|
||||
pub top: i32,
|
||||
/// The right point in a Win32 Rect
|
||||
/// Width of the recentangle (from the left point)
|
||||
pub right: i32,
|
||||
/// The bottom point in a Win32 Rect
|
||||
/// Height of the rectangle (from the top point)
|
||||
pub bottom: i32,
|
||||
}
|
||||
|
||||
#[cfg(feature = "win32")]
|
||||
impl From<RECT> for Rect {
|
||||
fn from(rect: RECT) -> Self {
|
||||
Self {
|
||||
@@ -26,6 +39,7 @@ impl From<RECT> for Rect {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "win32")]
|
||||
impl From<Rect> for RECT {
|
||||
fn from(rect: Rect) -> Self {
|
||||
Self {
|
||||
@@ -37,6 +51,53 @@ impl From<Rect> for RECT {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "darwin")]
|
||||
impl From<CGSize> for Rect {
|
||||
fn from(value: CGSize) -> Self {
|
||||
Self {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: value.width as i32,
|
||||
bottom: value.height as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "darwin")]
|
||||
impl From<CGRect> for Rect {
|
||||
fn from(value: CGRect) -> Self {
|
||||
Self {
|
||||
left: value.origin.x as i32,
|
||||
top: value.origin.y as i32,
|
||||
right: value.size.width as i32,
|
||||
bottom: value.size.height as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "darwin")]
|
||||
impl From<&Rect> for CGRect {
|
||||
fn from(value: &Rect) -> Self {
|
||||
Self {
|
||||
origin: CGPoint {
|
||||
x: value.left as CGFloat,
|
||||
y: value.top as CGFloat,
|
||||
},
|
||||
size: CGSize {
|
||||
width: value.right as CGFloat,
|
||||
height: value.bottom as CGFloat,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "darwin")]
|
||||
impl From<Rect> for CGRect {
|
||||
fn from(value: Rect) -> Self {
|
||||
CGRect::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||
self.right == rhs.right && self.bottom == rhs.bottom
|
||||
@@ -95,6 +156,7 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "win32")]
|
||||
#[must_use]
|
||||
pub const fn rect(&self) -> RECT {
|
||||
RECT {
|
||||
@@ -104,4 +166,19 @@ impl Rect {
|
||||
bottom: self.top + self.bottom,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "darwin")]
|
||||
#[must_use]
|
||||
pub fn percentage_within_horizontal_bounds(&self, other: &Rect) -> f64 {
|
||||
let overlap_left = self.left.max(other.left);
|
||||
let overlap_right = (self.left + self.right).min(other.left + other.right);
|
||||
|
||||
let overlap_width = overlap_right - overlap_left;
|
||||
|
||||
if overlap_width <= 0 {
|
||||
0.0
|
||||
} else {
|
||||
(overlap_width as f64) / (other.right as f64) * 100.0
|
||||
}
|
||||
}
|
||||
}
|
||||
31
komorebi-layouts/src/sizing.rs
Normal file
31
komorebi-layouts/src/sizing.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use clap::ValueEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Sizing
|
||||
pub enum Sizing {
|
||||
/// Increase
|
||||
Increase,
|
||||
/// Decrease
|
||||
Decrease,
|
||||
}
|
||||
|
||||
impl Sizing {
|
||||
#[must_use]
|
||||
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
|
||||
match self {
|
||||
Self::Increase => value + adjustment,
|
||||
Self::Decrease => {
|
||||
if value > 0 && value - adjustment >= 0 {
|
||||
value - adjustment
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.9" }
|
||||
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.9" }
|
||||
whkd-parser = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.10" }
|
||||
whkd-core = { git = "https://github.com/LGUG2Z/whkd", rev = "v0.2.10" }
|
||||
|
||||
eframe = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.39"
|
||||
version = "0.1.40"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "c9008bd5cfa288c926e9ea3aa18c92073f9281bd" }
|
||||
catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
|
||||
base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev = "b9e26b31f7a0e7ed239b14e5317e95d1bdc544bd" }
|
||||
#catppuccin-egui = { version = "5", default-features = false, features = ["egui32"] }
|
||||
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "b2f95cbf441d1dd99f3c955ef10dcb84ce23c20a", default-features = false, features = [
|
||||
"egui33",
|
||||
] }
|
||||
eframe = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use hex_color::HexColor;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::Schema;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::SchemaGenerator;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::InstanceType;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::Schema;
|
||||
#[cfg(feature = "schemars")]
|
||||
use schemars::schema::SchemaObject;
|
||||
|
||||
use crate::Color32;
|
||||
use serde::Deserialize;
|
||||
@@ -15,6 +11,7 @@ use serde::Serialize;
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Colour representation
|
||||
pub enum Colour {
|
||||
/// Colour represented as RGB
|
||||
Rgb(Rgb),
|
||||
@@ -56,22 +53,22 @@ impl From<Colour> for Color32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Colour represented as a Hex string
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Hex(pub HexColor);
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for Hex {
|
||||
fn schema_name() -> String {
|
||||
String::from("Hex")
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
std::borrow::Cow::Borrowed("Hex")
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
format: Some("color-hex".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
schemars::json_schema!({
|
||||
"type": "string",
|
||||
"format": "color-hex",
|
||||
"description": "Colour represented as a Hex string"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +83,7 @@ impl From<Colour> for u32 {
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Colour represented as RGB
|
||||
pub struct Rgb {
|
||||
/// Red
|
||||
pub r: u32,
|
||||
|
||||
@@ -12,9 +12,12 @@ use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Theme variant
|
||||
pub enum ThemeVariant {
|
||||
#[default]
|
||||
/// Dark variant
|
||||
Dark,
|
||||
/// Light variant
|
||||
Light,
|
||||
}
|
||||
|
||||
|
||||
@@ -28,18 +28,19 @@ use serde_variant::to_variant_name;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
/// Theme
|
||||
pub enum Theme {
|
||||
/// A theme from catppuccin-egui
|
||||
/// Theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
name: Catppuccin,
|
||||
accent: Option<CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
/// Theme from base16-egui-themes
|
||||
Base16 {
|
||||
name: Base16,
|
||||
accent: Option<Base16Value>,
|
||||
},
|
||||
/// A custom base16 palette
|
||||
/// Custom base16 palette
|
||||
Custom {
|
||||
palette: Box<Base16ColourPalette>,
|
||||
accent: Option<Base16Value>,
|
||||
@@ -47,22 +48,39 @@ pub enum Theme {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
/// Base16 colour palette: https://github.com/chriskempson/base16
|
||||
pub struct Base16ColourPalette {
|
||||
/// Base00
|
||||
pub base_00: Colour,
|
||||
/// Base01
|
||||
pub base_01: Colour,
|
||||
/// Base02
|
||||
pub base_02: Colour,
|
||||
/// Base03
|
||||
pub base_03: Colour,
|
||||
/// Base04
|
||||
pub base_04: Colour,
|
||||
/// Base05
|
||||
pub base_05: Colour,
|
||||
/// Base06
|
||||
pub base_06: Colour,
|
||||
/// Base07
|
||||
pub base_07: Colour,
|
||||
/// Base08
|
||||
pub base_08: Colour,
|
||||
/// Base09
|
||||
pub base_09: Colour,
|
||||
/// Base0A
|
||||
pub base_0a: Colour,
|
||||
/// Base0B
|
||||
pub base_0b: Colour,
|
||||
/// Base0C
|
||||
pub base_0c: Colour,
|
||||
/// Base0D
|
||||
pub base_0d: Colour,
|
||||
/// Base0E
|
||||
pub base_0e: Colour,
|
||||
/// Base0F
|
||||
pub base_0f: Colour,
|
||||
}
|
||||
|
||||
@@ -199,28 +217,48 @@ impl Theme {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
/// Base16 value
|
||||
pub enum Base16Value {
|
||||
/// Base00
|
||||
Base00,
|
||||
/// Base01
|
||||
Base01,
|
||||
/// Base02
|
||||
Base02,
|
||||
/// Base03
|
||||
Base03,
|
||||
/// Base04
|
||||
Base04,
|
||||
/// Base05
|
||||
Base05,
|
||||
/// Base06
|
||||
#[default]
|
||||
Base06,
|
||||
/// Base07
|
||||
Base07,
|
||||
/// Base08
|
||||
Base08,
|
||||
/// Base09
|
||||
Base09,
|
||||
/// Base0A
|
||||
Base0A,
|
||||
/// Base0B
|
||||
Base0B,
|
||||
/// Base0C
|
||||
Base0C,
|
||||
/// Base0D
|
||||
Base0D,
|
||||
/// Base0E
|
||||
Base0E,
|
||||
/// Base0F
|
||||
Base0F,
|
||||
}
|
||||
|
||||
/// Wrapper around a Base16 colour palette
|
||||
pub enum Base16Wrapper {
|
||||
/// Predefined Base16 colour palette
|
||||
Base16(Base16),
|
||||
/// Custom Base16 colour palette
|
||||
Custom(Box<Base16ColourPalette>),
|
||||
}
|
||||
|
||||
@@ -268,10 +306,15 @@ impl Base16Value {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
/// Catppuccin palette
|
||||
pub enum Catppuccin {
|
||||
/// Frappe (https://catppuccin.com/palette#flavor-frappe)
|
||||
Frappe,
|
||||
/// Latte (https://catppuccin.com/palette#flavor-latte)
|
||||
Latte,
|
||||
/// Macchiato (https://catppuccin.com/palette#flavor-macchiato)
|
||||
Macchiato,
|
||||
/// Mocha (https://catppuccin.com/palette#flavor-mocha)
|
||||
Mocha,
|
||||
}
|
||||
|
||||
@@ -293,33 +336,60 @@ impl From<Catppuccin> for catppuccin_egui::Theme {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, Display, PartialEq)]
|
||||
/// Catppuccin Value
|
||||
pub enum CatppuccinValue {
|
||||
/// Rosewater
|
||||
Rosewater,
|
||||
/// Flamingo
|
||||
Flamingo,
|
||||
/// Pink
|
||||
Pink,
|
||||
/// Mauve
|
||||
Mauve,
|
||||
/// Red
|
||||
Red,
|
||||
/// Maroon
|
||||
Maroon,
|
||||
/// Peach
|
||||
Peach,
|
||||
/// Yellow
|
||||
Yellow,
|
||||
/// Green
|
||||
Green,
|
||||
/// Teal
|
||||
Teal,
|
||||
/// Sky
|
||||
Sky,
|
||||
/// Sapphire
|
||||
Sapphire,
|
||||
/// Blue
|
||||
Blue,
|
||||
/// Lavender
|
||||
Lavender,
|
||||
#[default]
|
||||
/// Text
|
||||
Text,
|
||||
/// Subtext1
|
||||
Subtext1,
|
||||
/// Subtext0
|
||||
Subtext0,
|
||||
/// Overlay2
|
||||
Overlay2,
|
||||
/// Overlay1
|
||||
Overlay1,
|
||||
/// Overlay0
|
||||
Overlay0,
|
||||
/// Surface2
|
||||
Surface2,
|
||||
/// Surface1
|
||||
Surface1,
|
||||
/// Surface0
|
||||
Surface0,
|
||||
/// Base
|
||||
Base,
|
||||
/// Mantle
|
||||
Mantle,
|
||||
/// Crust
|
||||
Crust,
|
||||
}
|
||||
|
||||
@@ -359,3 +429,275 @@ impl CatppuccinValue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Theme from catppuccin-egui
|
||||
pub struct KomorebiThemeCatppuccin {
|
||||
/// Name of the Catppuccin theme (previews: https://github.com/catppuccin/catppuccin)
|
||||
pub name: Catppuccin,
|
||||
/// Single window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||
pub single_border: Option<CatppuccinValue>,
|
||||
/// Stack window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Green)))]
|
||||
pub stack_border: Option<CatppuccinValue>,
|
||||
/// Monocle window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Pink)))]
|
||||
pub monocle_border: Option<CatppuccinValue>,
|
||||
/// Floating window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Yellow)))]
|
||||
pub floating_border: Option<CatppuccinValue>,
|
||||
/// Unfocused window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Base)))]
|
||||
pub unfocused_border: Option<CatppuccinValue>,
|
||||
/// Unfocused locked window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Red)))]
|
||||
pub unfocused_locked_border: Option<CatppuccinValue>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar focused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Green)))]
|
||||
pub stackbar_focused_text: Option<CatppuccinValue>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar unfocused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Text)))]
|
||||
pub stackbar_unfocused_text: Option<CatppuccinValue>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar background colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Base)))]
|
||||
pub stackbar_background: Option<CatppuccinValue>,
|
||||
/// Bar accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||
pub bar_accent: Option<CatppuccinValue>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Theme from base16-egui-themes
|
||||
pub struct KomorebiThemeBase16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
pub name: Base16,
|
||||
/// Single window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||
pub single_border: Option<Base16Value>,
|
||||
/// Stack window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||
pub stack_border: Option<Base16Value>,
|
||||
/// Monocle window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0F)))]
|
||||
pub monocle_border: Option<Base16Value>,
|
||||
/// Floating window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base09)))]
|
||||
pub floating_border: Option<Base16Value>,
|
||||
/// Unfocused window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||
pub unfocused_border: Option<Base16Value>,
|
||||
/// Unfocused locked window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base08)))]
|
||||
pub unfocused_locked_border: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar focused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||
pub stackbar_focused_text: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar unfocused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base05)))]
|
||||
pub stackbar_unfocused_text: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar background colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||
pub stackbar_background: Option<Base16Value>,
|
||||
/// Bar accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||
pub bar_accent: Option<Base16Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Custom Base16 theme
|
||||
pub struct KomorebiThemeCustom {
|
||||
/// Colours of the custom Base16 theme palette
|
||||
pub colours: Box<Base16ColourPalette>,
|
||||
/// Single window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||
pub single_border: Option<Base16Value>,
|
||||
/// Stack window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||
pub stack_border: Option<Base16Value>,
|
||||
/// Monocle window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0F)))]
|
||||
pub monocle_border: Option<Base16Value>,
|
||||
/// Floating window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base09)))]
|
||||
pub floating_border: Option<Base16Value>,
|
||||
/// Unfocused window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||
pub unfocused_border: Option<Base16Value>,
|
||||
/// Unfocused locked window border colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base08)))]
|
||||
pub unfocused_locked_border: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar focused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0B)))]
|
||||
pub stackbar_focused_text: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar unfocused text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base05)))]
|
||||
pub stackbar_unfocused_text: Option<Base16Value>,
|
||||
#[cfg(target_os = "windows")]
|
||||
/// Stackbar background colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base01)))]
|
||||
pub stackbar_background: Option<Base16Value>,
|
||||
/// Bar accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||
pub bar_accent: Option<Base16Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
/// Komorebi theme
|
||||
pub enum KomorebiTheme {
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Catppuccin"))]
|
||||
/// Theme from catppuccin-egui
|
||||
Catppuccin(KomorebiThemeCatppuccin),
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Base16"))]
|
||||
/// Theme from base16-egui-themes
|
||||
Base16(KomorebiThemeBase16),
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
/// Custom Base16 theme
|
||||
Custom(KomorebiThemeCustom),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Theme from catppuccin-egui
|
||||
pub struct KomobarThemeCatppuccin {
|
||||
/// Name of the Catppuccin theme (previews: https://github.com/catppuccin/catppuccin)
|
||||
pub name: Catppuccin,
|
||||
/// Accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||
pub accent: Option<CatppuccinValue>,
|
||||
/// Auto select fill colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_fill: Option<CatppuccinValue>,
|
||||
/// Auto select text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_text: Option<CatppuccinValue>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Theme from base16-egui-themes
|
||||
pub struct KomobarThemeBase16 {
|
||||
/// Name of the Base16 theme (previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
pub name: Base16,
|
||||
/// Accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Base16Value::Base0D)))]
|
||||
pub accent: Option<Base16Value>,
|
||||
/// Auto select fill colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_fill: Option<Base16Value>,
|
||||
/// Auto select text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_text: Option<Base16Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
/// Theme from base16-egui-themes
|
||||
pub struct KomobarThemeCustom {
|
||||
/// Colours of the custom Base16 theme palette
|
||||
pub colours: Box<Base16ColourPalette>,
|
||||
/// Accent colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CatppuccinValue::Blue)))]
|
||||
pub accent: Option<Base16Value>,
|
||||
/// Auto select fill colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_fill: Option<Base16Value>,
|
||||
/// Auto select text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_select_text: Option<Base16Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
/// Komorebi bar theme
|
||||
pub enum KomobarTheme {
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Catppuccin"))]
|
||||
/// Theme from catppuccin-egui
|
||||
Catppuccin(KomobarThemeCatppuccin),
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Base16"))]
|
||||
/// Theme from base16-egui-themes
|
||||
Base16(KomobarThemeBase16),
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
/// Custom Base16 theme
|
||||
Custom(KomobarThemeCustom),
|
||||
}
|
||||
|
||||
impl From<KomorebiTheme> for KomobarTheme {
|
||||
fn from(value: KomorebiTheme) -> Self {
|
||||
match value {
|
||||
KomorebiTheme::Catppuccin(KomorebiThemeCatppuccin {
|
||||
name, bar_accent, ..
|
||||
}) => Self::Catppuccin(KomobarThemeCatppuccin {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
}),
|
||||
KomorebiTheme::Base16(KomorebiThemeBase16 {
|
||||
name, bar_accent, ..
|
||||
}) => Self::Base16(KomobarThemeBase16 {
|
||||
name,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
}),
|
||||
KomorebiTheme::Custom(KomorebiThemeCustom {
|
||||
colours,
|
||||
bar_accent,
|
||||
..
|
||||
}) => Self::Custom(KomobarThemeCustom {
|
||||
colours,
|
||||
accent: bar_accent,
|
||||
auto_select_fill: None,
|
||||
auto_select_text: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.39"
|
||||
version = "0.1.40"
|
||||
description = "A tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2024"
|
||||
@@ -8,15 +8,19 @@ edition = "2024"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
komorebi-layouts = { path = "../komorebi-layouts", features = ["win32"] }
|
||||
komorebi-themes = { path = "../komorebi-themes" }
|
||||
|
||||
base64 = "0.22"
|
||||
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"
|
||||
hotwatch = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
miow = "0.6"
|
||||
@@ -27,9 +31,10 @@ parking_lot = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
powershell_script = "1.0"
|
||||
regex = "1"
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
serde_yaml = { workspace = true }
|
||||
shadow-rs = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
@@ -47,7 +52,7 @@ windows-implement = { workspace = true }
|
||||
windows-interface = { workspace = true }
|
||||
winput = "0.2"
|
||||
winreg = "0.55"
|
||||
serde_with = { version = "3.12", features = ["schemars_0_8"] }
|
||||
serde_with = { version = "3.12", features = ["schemars_1"] }
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { workspace = true }
|
||||
@@ -59,4 +64,4 @@ uuid = { version = "1", features = ["v4"] }
|
||||
[features]
|
||||
default = ["schemars"]
|
||||
deadlock_detection = ["parking_lot/deadlock_detection"]
|
||||
schemars = ["dep:schemars"]
|
||||
schemars = ["dep:schemars", "komorebi-layouts/schemars"]
|
||||
|
||||
@@ -25,8 +25,33 @@ use serde::Serialize;
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Animation configuration
|
||||
///
|
||||
/// This can be either global:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "enabled": true,
|
||||
/// "style": "EaseInSine",
|
||||
/// "fps": 60,
|
||||
/// "duration": 250
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Or scoped by an animation kind prefix:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "movement": {
|
||||
/// "enabled": true,
|
||||
/// "style": "EaseInSine",
|
||||
/// "fps": 60,
|
||||
/// "duration": 250
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub enum PerAnimationPrefixConfig<T> {
|
||||
/// Animation configuration prefixed for a specific animation kind
|
||||
Prefix(HashMap<AnimationPrefix, T>),
|
||||
/// Animation configuration for all animation kinds
|
||||
Global(T),
|
||||
}
|
||||
|
||||
|
||||
@@ -420,6 +420,7 @@ pub fn apply_ease_func(t: f64, style: AnimationStyle) -> f64 {
|
||||
AnimationStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
|
||||
AnimationStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
|
||||
AnimationStyle::EaseInCubic => EaseInCubic::evaluate(t),
|
||||
AnimationStyle::EaseOutCubic => EaseOutCubic::evaluate(t),
|
||||
AnimationStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
|
||||
AnimationStyle::EaseInQuart => EaseInQuart::evaluate(t),
|
||||
AnimationStyle::EaseOutQuart => EaseOutQuart::evaluate(t),
|
||||
|
||||
@@ -11,7 +11,9 @@ use crate::core::Rect;
|
||||
use crate::windows_api;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::mpsc;
|
||||
use windows::Win32::Foundation::FALSE;
|
||||
@@ -126,6 +128,7 @@ pub struct Border {
|
||||
pub brush_properties: D2D1_BRUSH_PROPERTIES,
|
||||
pub rounded_rect: D2D1_ROUNDED_RECT,
|
||||
pub brushes: HashMap<WindowKind, ID2D1SolidColorBrush>,
|
||||
pub is_destroying: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl From<isize> for Border {
|
||||
@@ -144,6 +147,7 @@ impl From<isize> for Border {
|
||||
brush_properties: D2D1_BRUSH_PROPERTIES::default(),
|
||||
rounded_rect: D2D1_ROUNDED_RECT::default(),
|
||||
brushes: HashMap::new(),
|
||||
is_destroying: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,6 +196,7 @@ impl Border {
|
||||
brush_properties: Default::default(),
|
||||
rounded_rect: Default::default(),
|
||||
brushes: HashMap::new(),
|
||||
is_destroying: Arc::new(AtomicBool::new(false)),
|
||||
};
|
||||
|
||||
let border_pointer = &raw mut border;
|
||||
@@ -313,6 +318,17 @@ impl Border {
|
||||
}
|
||||
|
||||
pub fn destroy(&self) -> color_eyre::Result<()> {
|
||||
// signal that we're destroying - prevents new render operations
|
||||
self.is_destroying.store(true, Ordering::Release);
|
||||
|
||||
// small delay to allow in-flight render operations to complete
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
// clear user data **BEFORE** closing window
|
||||
// pending messages will see a null pointer and exit early
|
||||
unsafe {
|
||||
SetWindowLongPtrW(self.hwnd(), GWLP_USERDATA, 0);
|
||||
}
|
||||
WindowsApi::close_window(self.hwnd)
|
||||
}
|
||||
|
||||
@@ -381,6 +397,10 @@ impl Border {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||
|
||||
let old_rect = (*border_pointer).window_rect;
|
||||
@@ -395,6 +415,11 @@ impl Border {
|
||||
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()
|
||||
{
|
||||
// double-check destruction flag before rendering
|
||||
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
@@ -463,6 +488,10 @@ impl Border {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
let reference_hwnd = (*border_pointer).tracking_hwnd;
|
||||
|
||||
// Update position to update the ZOrder
|
||||
@@ -476,6 +505,11 @@ impl Border {
|
||||
}
|
||||
|
||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||
// double-check destruction flag before rendering
|
||||
if (*border_pointer).is_destroying.load(Ordering::Acquire) {
|
||||
return LRESULT(0);
|
||||
}
|
||||
|
||||
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
|
||||
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
|
||||
|
||||
@@ -543,7 +577,12 @@ impl Border {
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||
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);
|
||||
}
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
|
||||
@@ -767,6 +767,7 @@ 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(())
|
||||
@@ -813,10 +814,26 @@ pub fn hide_border(tracking_hwnd: isize) {
|
||||
|
||||
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Z Order (https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos)
|
||||
pub enum ZOrder {
|
||||
/// HWND_TOP
|
||||
///
|
||||
/// Places the window at the top of the Z order.
|
||||
Top,
|
||||
/// HWND_NOTOPMOST
|
||||
///
|
||||
/// Places the window above all non-topmost windows (that is, behind all topmost windows).
|
||||
/// This flag has no effect if the window is already a non-topmost window.
|
||||
NoTopMost,
|
||||
/// HWND_BOTTOM
|
||||
///
|
||||
/// Places the window at the bottom of the Z order. If the hWnd parameter identifies a topmost window,
|
||||
/// the window loses its topmost status and is placed at the bottom of all other windows.
|
||||
Bottom,
|
||||
/// HWND_TOPMOST
|
||||
///
|
||||
/// Places the window above all non-topmost windows.
|
||||
/// The window maintains its topmost position even when it is deactivated.
|
||||
TopMost,
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,18 @@ impl Lockable for Container {
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn preselect() -> Self {
|
||||
Self {
|
||||
id: "PRESELECT".to_string(),
|
||||
locked: false,
|
||||
windows: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_preselect(&self) -> bool {
|
||||
self.id == "PRESELECT"
|
||||
}
|
||||
|
||||
pub fn hide(&self, omit: Option<isize>) {
|
||||
for window in self.windows().iter().rev() {
|
||||
let mut should_hide = omit.is_none();
|
||||
|
||||
@@ -8,38 +8,73 @@ 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),
|
||||
}
|
||||
|
||||
|
||||
@@ -53,41 +53,64 @@ impl ApplicationOptions {
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Rule for matching applications
|
||||
pub enum MatchingRule {
|
||||
/// Simple matching rule which must evaluate to true
|
||||
Simple(IdWithIdentifier),
|
||||
/// Composite matching rule where all conditions must evaluate to true
|
||||
Composite(Vec<IdWithIdentifier>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Rule for assigning applications to a workspace
|
||||
pub struct WorkspaceMatchingRule {
|
||||
/// Target monitor index
|
||||
pub monitor_index: usize,
|
||||
/// Target workspace index
|
||||
pub workspace_index: usize,
|
||||
/// Matching rule for the application
|
||||
pub matching_rule: MatchingRule,
|
||||
/// Whether to apply the rule only when the application is initially launched
|
||||
pub initial_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Rule for matching applications
|
||||
pub struct IdWithIdentifier {
|
||||
/// Kind of identifier to target
|
||||
pub kind: ApplicationIdentifier,
|
||||
/// Target identifier
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Matching strategy to use
|
||||
pub matching_strategy: Option<MatchingStrategy>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Strategy for matching identifiers
|
||||
pub enum MatchingStrategy {
|
||||
/// Should not be used, only kept for backward compatibility
|
||||
Legacy,
|
||||
/// Equals
|
||||
Equals,
|
||||
/// Starts With
|
||||
StartsWith,
|
||||
/// Ends With
|
||||
EndsWith,
|
||||
/// Contains
|
||||
Contains,
|
||||
/// Regex
|
||||
Regex,
|
||||
/// Does not end with
|
||||
DoesNotEndWith,
|
||||
/// Does not start with
|
||||
DoesNotStartWith,
|
||||
/// Does not equal
|
||||
DoesNotEqual,
|
||||
/// Does not contain
|
||||
DoesNotContain,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
use clap::ValueEnum;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use strum::EnumString;
|
||||
|
||||
use super::OperationDirection;
|
||||
use super::Rect;
|
||||
use super::Sizing;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum DefaultLayout {
|
||||
BSP,
|
||||
Columns,
|
||||
Rows,
|
||||
VerticalStack,
|
||||
HorizontalStack,
|
||||
UltrawideVerticalStack,
|
||||
Grid,
|
||||
RightMainVerticalStack,
|
||||
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))]
|
||||
pub struct LayoutOptions {
|
||||
/// Options related to the Scrolling layout
|
||||
pub scrolling: Option<ScrollingLayoutOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
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>,
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
Self::UltrawideVerticalStack | Self::RightMainVerticalStack => match len {
|
||||
n if n > 1 => 1,
|
||||
_ => 0,
|
||||
},
|
||||
Self::Scrolling => 0,
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::HorizontalStack
|
||||
| DefaultLayout::Grid => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rightmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
| DefaultLayout::VerticalStack
|
||||
| DefaultLayout::HorizontalStack
|
||||
| DefaultLayout::Grid => len.saturating_sub(1),
|
||||
DefaultLayout::UltrawideVerticalStack => match len {
|
||||
2 => 0,
|
||||
_ => len.saturating_sub(1),
|
||||
},
|
||||
DefaultLayout::RightMainVerticalStack => 0,
|
||||
DefaultLayout::Scrolling => len.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
|
||||
pub fn resize(
|
||||
&self,
|
||||
unaltered: &Rect,
|
||||
resize: &Option<Rect>,
|
||||
edge: OperationDirection,
|
||||
sizing: Sizing,
|
||||
delta: i32,
|
||||
) -> Option<Rect> {
|
||||
if !matches!(
|
||||
self,
|
||||
Self::BSP
|
||||
| Self::Columns
|
||||
| Self::Rows
|
||||
| Self::VerticalStack
|
||||
| Self::RightMainVerticalStack
|
||||
| Self::HorizontalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::Scrolling
|
||||
) {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut r = resize.unwrap_or_default();
|
||||
|
||||
let resize_delta = delta;
|
||||
|
||||
match edge {
|
||||
OperationDirection::Left => match sizing {
|
||||
Sizing::Increase => {
|
||||
// Some final checks to make sure the user can't infinitely resize to
|
||||
// the point of pushing other windows out of bounds
|
||||
|
||||
// Note: These checks cannot take into account the changes made to the
|
||||
// edges of adjacent windows at operation time, so it is still possible
|
||||
// to push windows out of bounds by maxing out an Increase Left on a
|
||||
// Window with index 1, and then maxing out a Decrease Right on a Window
|
||||
// with index 0. I don't think it's worth trying to defensively program
|
||||
// against this; if people end up in this situation they are better off
|
||||
// just hitting the retile command
|
||||
let diff = ((r.left + -resize_delta) as f32).abs();
|
||||
if diff < unaltered.right as f32 {
|
||||
r.left += -resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.left - -resize_delta) as f32).abs();
|
||||
if diff < unaltered.right as f32 {
|
||||
r.left -= -resize_delta;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Up => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.top + resize_delta) as f32).abs();
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.top += -resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.top - resize_delta) as f32).abs();
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.top -= -resize_delta;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Right => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.right + resize_delta) as f32).abs();
|
||||
if diff < unaltered.right as f32 {
|
||||
r.right += resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.right - resize_delta) as f32).abs();
|
||||
if diff < unaltered.right as f32 {
|
||||
r.right -= resize_delta;
|
||||
}
|
||||
}
|
||||
},
|
||||
OperationDirection::Down => match sizing {
|
||||
Sizing::Increase => {
|
||||
let diff = ((r.bottom + resize_delta) as f32).abs();
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.bottom += resize_delta;
|
||||
}
|
||||
}
|
||||
Sizing::Decrease => {
|
||||
let diff = ((r.bottom - resize_delta) as f32).abs();
|
||||
if diff < unaltered.bottom as f32 {
|
||||
r.bottom -= resize_delta;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if r.eq(&Rect::default()) {
|
||||
None
|
||||
} else {
|
||||
Option::from(r)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn cycle_next(self) -> Self {
|
||||
match self {
|
||||
Self::BSP => Self::Columns,
|
||||
Self::Columns => Self::Rows,
|
||||
Self::Rows => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Scrolling,
|
||||
Self::Scrolling => Self::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn cycle_previous(self) -> Self {
|
||||
match self {
|
||||
Self::Scrolling => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||
Self::HorizontalStack => Self::VerticalStack,
|
||||
Self::VerticalStack => Self::Rows,
|
||||
Self::Rows => Self::Columns,
|
||||
Self::Columns => Self::BSP,
|
||||
Self::BSP => Self::RightMainVerticalStack,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#![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;
|
||||
@@ -14,37 +15,43 @@ use strum::EnumString;
|
||||
|
||||
use crate::KomorebiTheme;
|
||||
use crate::animation::prefix::AnimationPrefix;
|
||||
|
||||
// Re-export everything from komorebi-layouts
|
||||
pub use komorebi_layouts::Arrangement;
|
||||
pub use komorebi_layouts::Axis;
|
||||
pub use komorebi_layouts::Column;
|
||||
pub use komorebi_layouts::ColumnSplit;
|
||||
pub use komorebi_layouts::ColumnSplitWithCapacity;
|
||||
pub use komorebi_layouts::ColumnWidth;
|
||||
pub use komorebi_layouts::CustomLayout;
|
||||
pub use komorebi_layouts::CycleDirection;
|
||||
pub use komorebi_layouts::DEFAULT_RATIO;
|
||||
pub use komorebi_layouts::DEFAULT_SECONDARY_RATIO;
|
||||
pub use komorebi_layouts::DefaultLayout;
|
||||
pub use komorebi_layouts::Direction;
|
||||
pub use komorebi_layouts::GridLayoutOptions;
|
||||
pub use komorebi_layouts::Layout;
|
||||
pub use komorebi_layouts::LayoutOptions;
|
||||
pub use komorebi_layouts::MAX_RATIO;
|
||||
pub use komorebi_layouts::MAX_RATIOS;
|
||||
pub use komorebi_layouts::MIN_RATIO;
|
||||
pub use komorebi_layouts::OperationDirection;
|
||||
pub use komorebi_layouts::Rect;
|
||||
pub use komorebi_layouts::ScrollingLayoutOptions;
|
||||
pub use komorebi_layouts::Sizing;
|
||||
pub use komorebi_layouts::validate_ratios;
|
||||
|
||||
// Local modules and exports
|
||||
pub use animation::AnimationStyle;
|
||||
pub use arrangement::Arrangement;
|
||||
pub use arrangement::Axis;
|
||||
pub use custom_layout::Column;
|
||||
pub use custom_layout::ColumnSplit;
|
||||
pub use custom_layout::ColumnSplitWithCapacity;
|
||||
pub use custom_layout::ColumnWidth;
|
||||
pub use custom_layout::CustomLayout;
|
||||
pub use cycle_direction::CycleDirection;
|
||||
pub use default_layout::DefaultLayout;
|
||||
pub use direction::Direction;
|
||||
pub use layout::Layout;
|
||||
pub use operation_direction::OperationDirection;
|
||||
pub use pathext::PathExt;
|
||||
pub use pathext::ResolvedPathBuf;
|
||||
pub use pathext::replace_env_in_path;
|
||||
pub use pathext::resolve_option_hashmap_usize_path;
|
||||
pub use rect::Rect;
|
||||
|
||||
pub mod animation;
|
||||
pub mod arrangement;
|
||||
pub mod asc;
|
||||
pub mod config_generation;
|
||||
pub mod custom_layout;
|
||||
pub mod cycle_direction;
|
||||
pub mod default_layout;
|
||||
pub mod direction;
|
||||
pub mod layout;
|
||||
pub mod operation_direction;
|
||||
pub mod pathext;
|
||||
pub mod rect;
|
||||
|
||||
// serde_as must be before derive
|
||||
#[serde_with::serde_as]
|
||||
@@ -55,6 +62,8 @@ pub enum SocketMessage {
|
||||
// Window / Container Commands
|
||||
FocusWindow(OperationDirection),
|
||||
MoveWindow(OperationDirection),
|
||||
PreselectDirection(OperationDirection),
|
||||
CancelPreselect,
|
||||
CycleFocusWindow(CycleDirection),
|
||||
CycleMoveWindow(CycleDirection),
|
||||
StackWindow(OperationDirection),
|
||||
@@ -87,6 +96,7 @@ pub enum SocketMessage {
|
||||
Close,
|
||||
Minimize,
|
||||
Promote,
|
||||
PromoteSwap,
|
||||
PromoteFocus,
|
||||
PromoteWindow(OperationDirection),
|
||||
EagerFocus(String),
|
||||
@@ -109,6 +119,7 @@ pub enum SocketMessage {
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(DefaultLayout),
|
||||
CycleLayout(CycleDirection),
|
||||
LayoutRatios(Option<Vec<f32>>, Option<Vec<f32>>),
|
||||
ScrollingLayoutColumns(NonZeroUsize),
|
||||
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
FlipLayout(Axis),
|
||||
@@ -270,17 +281,24 @@ pub struct SubscribeOptions {
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Stackbar mode
|
||||
pub enum StackbarMode {
|
||||
/// Always show
|
||||
Always,
|
||||
/// Never show
|
||||
Never,
|
||||
/// Show on stack
|
||||
OnStack,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Starbar label
|
||||
pub enum StackbarLabel {
|
||||
#[default]
|
||||
/// Process name
|
||||
Process,
|
||||
/// Window title
|
||||
Title,
|
||||
}
|
||||
|
||||
@@ -288,6 +306,7 @@ pub enum StackbarLabel {
|
||||
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Border style
|
||||
pub enum BorderStyle {
|
||||
#[default]
|
||||
/// Use the system border style
|
||||
@@ -302,6 +321,7 @@ pub enum BorderStyle {
|
||||
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Border style
|
||||
pub enum BorderImplementation {
|
||||
#[default]
|
||||
/// Use the adjustable komorebi border implementation
|
||||
@@ -325,13 +345,20 @@ pub enum BorderImplementation {
|
||||
Hash,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Window kind
|
||||
pub enum WindowKind {
|
||||
/// Single window
|
||||
Single,
|
||||
/// Stack container
|
||||
Stack,
|
||||
/// Monocle container
|
||||
Monocle,
|
||||
#[default]
|
||||
/// Unfocused window
|
||||
Unfocused,
|
||||
/// Unfocused locked container
|
||||
UnfocusedLocked,
|
||||
/// Floating window
|
||||
Floating,
|
||||
}
|
||||
|
||||
@@ -352,30 +379,37 @@ pub enum StateQuery {
|
||||
Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Application identifier
|
||||
pub enum ApplicationIdentifier {
|
||||
/// Executable name
|
||||
#[serde(alias = "exe")]
|
||||
Exe,
|
||||
/// Class
|
||||
#[serde(alias = "class")]
|
||||
Class,
|
||||
#[serde(alias = "title")]
|
||||
/// Window title
|
||||
Title,
|
||||
/// Executable path
|
||||
#[serde(alias = "path")]
|
||||
Path,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Focus follows mouse implementation
|
||||
pub enum FocusFollowsMouseImplementation {
|
||||
/// A custom FFM implementation (slightly more CPU-intensive)
|
||||
/// Custom FFM implementation (slightly more CPU-intensive)
|
||||
Komorebi,
|
||||
/// The native (legacy) Windows FFM implementation
|
||||
/// Native (legacy) Windows FFM implementation
|
||||
Windows,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Window management behaviour
|
||||
pub struct WindowManagementBehaviour {
|
||||
/// The current WindowContainerBehaviour to be used
|
||||
/// The current [`WindowContainerBehaviour`] to be used
|
||||
pub current_behaviour: WindowContainerBehaviour,
|
||||
/// Override of `current_behaviour` to open new windows as floating windows
|
||||
/// that can be later toggled to tiled, when false it will default to
|
||||
@@ -394,7 +428,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,
|
||||
}
|
||||
|
||||
@@ -402,6 +436,7 @@ 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]
|
||||
@@ -414,6 +449,7 @@ 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]
|
||||
@@ -426,6 +462,7 @@ 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]
|
||||
@@ -465,6 +502,7 @@ 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]
|
||||
@@ -479,6 +517,7 @@ 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,
|
||||
@@ -489,10 +528,12 @@ 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)
|
||||
/// END OF LIFE FEATURE: Use the `SW_HIDE` flag to hide windows when switching workspaces (has issues with Electron apps)
|
||||
#[deprecated(note = "End of life feature")]
|
||||
Hide,
|
||||
/// Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
/// Use the `SW_MINIMIZE` flag to hide windows when switching workspaces (has issues with frequent workspace switching)
|
||||
Minimize,
|
||||
/// Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
|
||||
Cloak,
|
||||
@@ -502,44 +543,25 @@ 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 komorebic commands on temporarily unmanaged/floated windows
|
||||
/// Process commands on temporarily unmanaged/floated windows
|
||||
#[default]
|
||||
Op,
|
||||
/// Ignore komorebic commands on temporarily unmanaged/floated windows
|
||||
/// Ignore commands on temporarily unmanaged/floated windows
|
||||
NoOp,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum Sizing {
|
||||
Increase,
|
||||
Decrease,
|
||||
}
|
||||
|
||||
impl Sizing {
|
||||
#[must_use]
|
||||
pub const fn adjust_by(&self, value: i32, adjustment: i32) -> i32 {
|
||||
match self {
|
||||
Self::Increase => value + adjustment,
|
||||
Self::Decrease => {
|
||||
if value > 0 && value - adjustment >= 0 {
|
||||
value - adjustment
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Window handling behaviour
|
||||
pub enum WindowHandlingBehaviour {
|
||||
#[default]
|
||||
/// Synchronous
|
||||
Sync,
|
||||
/// Asynchronous
|
||||
Async,
|
||||
}
|
||||
|
||||
|
||||
@@ -121,13 +121,16 @@ impl<'de> serde_with::DeserializeAs<'de, PathBuf> for ResolvedPathBuf {
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl serde_with::schemars_0_8::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
|
||||
fn schema_name() -> String {
|
||||
"PathBuf".to_owned()
|
||||
impl serde_with::schemars_1::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
|
||||
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||
std::borrow::Cow::Borrowed("PathBuf")
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::schema::Schema {
|
||||
<PathBuf as schemars::JsonSchema>::json_schema(generator)
|
||||
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."
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ pub mod process_event;
|
||||
pub mod process_movement;
|
||||
pub mod reaper;
|
||||
pub mod set_window_position;
|
||||
pub mod splash;
|
||||
pub mod stackbar_manager;
|
||||
pub mod state;
|
||||
pub mod static_config;
|
||||
pub mod styles;
|
||||
pub mod theme_manager;
|
||||
@@ -70,6 +72,7 @@ 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;
|
||||
@@ -239,7 +242,9 @@ 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);
|
||||
@@ -253,6 +258,21 @@ 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.
|
||||
|
||||
@@ -50,10 +50,10 @@ 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;
|
||||
@@ -176,7 +176,7 @@ struct Opts {
|
||||
/// Allow the use of komorebi's custom focus-follows-mouse implementation
|
||||
#[clap(short, long = "ffm")]
|
||||
focus_follows_mouse: bool,
|
||||
/// Wait for 'komorebic complete-configuration' to be sent before processing events
|
||||
/// Wait for `komorebic complete-configuration` to be sent before processing events
|
||||
#[clap(short, long)]
|
||||
await_configuration: bool,
|
||||
/// Start a TCP server on the given port to allow the direct sending of SocketMessages
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
use crate::WindowManager;
|
||||
use crate::WindowsApi;
|
||||
@@ -15,6 +14,7 @@ use crate::monitor;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::monitor_reconciliator::hidden::Hidden;
|
||||
use crate::notify_subscribers;
|
||||
use crate::state::State;
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicConsume;
|
||||
@@ -25,6 +25,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub mod hidden;
|
||||
@@ -44,6 +45,10 @@ pub enum MonitorNotification {
|
||||
|
||||
static ACTIVE: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
/// Timestamp (epoch millis) of the last DisplayConnectionChange notification.
|
||||
/// Used to suppress OS-initiated window minimizes during transient display events.
|
||||
static LAST_DISPLAY_CHANGE_TIMESTAMP: AtomicI64 = AtomicI64::new(0);
|
||||
|
||||
static CHANNEL: OnceLock<(Sender<MonitorNotification>, Receiver<MonitorNotification>)> =
|
||||
OnceLock::new();
|
||||
|
||||
@@ -62,11 +67,40 @@ fn event_rx() -> Receiver<MonitorNotification> {
|
||||
}
|
||||
|
||||
pub fn send_notification(notification: MonitorNotification) {
|
||||
if matches!(
|
||||
notification,
|
||||
MonitorNotification::DisplayConnectionChange
|
||||
| MonitorNotification::ResumingFromSuspendedState
|
||||
| MonitorNotification::SessionUnlocked
|
||||
) {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
LAST_DISPLAY_CHANGE_TIMESTAMP.store(now, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if event_tx().try_send(notification).is_err() {
|
||||
tracing::warn!("channel is full; dropping notification")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if a display connection change event was received within the
|
||||
/// last `grace_period` duration. This is used by the event processor to avoid
|
||||
/// treating OS-initiated minimizes (caused by transient monitor disconnects)
|
||||
/// as user-initiated minimizes.
|
||||
pub fn display_change_in_progress(grace_period: std::time::Duration) -> bool {
|
||||
let last = LAST_DISPLAY_CHANGE_TIMESTAMP.load(Ordering::SeqCst);
|
||||
if last == 0 {
|
||||
return false;
|
||||
}
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
(now - last) < grace_period.as_millis() as i64
|
||||
}
|
||||
|
||||
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
|
||||
let dip = DISPLAY_INDEX_PREFERENCES.read();
|
||||
let mut dip_ids = dip.values();
|
||||
@@ -89,7 +123,41 @@ where
|
||||
F: Fn() -> I + Copy,
|
||||
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
|
||||
{
|
||||
let all_displays = display_provider().flatten().collect::<Vec<_>>();
|
||||
let mut attempts = 0;
|
||||
|
||||
let (displays, errors) = loop {
|
||||
let (displays, errors): (Vec<_>, Vec<_>) = display_provider().partition(Result::is_ok);
|
||||
|
||||
if errors.is_empty() {
|
||||
break (displays, errors);
|
||||
}
|
||||
|
||||
for err in &errors {
|
||||
if let Err(e) = err {
|
||||
tracing::warn!(
|
||||
"enumerating display in reconciliator (attempt {}): {:?}",
|
||||
attempts + 1,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if attempts < 5 {
|
||||
attempts += 1;
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
continue;
|
||||
}
|
||||
|
||||
break (displays, errors);
|
||||
};
|
||||
|
||||
if !errors.is_empty() {
|
||||
return Err(color_eyre::eyre::eyre!(
|
||||
"could not successfully enumerate all displays"
|
||||
));
|
||||
}
|
||||
|
||||
let all_displays = displays.into_iter().map(Result::unwrap).collect::<Vec<_>>();
|
||||
|
||||
let mut serial_id_map = HashMap::new();
|
||||
|
||||
@@ -203,6 +271,8 @@ where
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
|
||||
// Keep reference to Arc for potential re-locking
|
||||
let wm_arc = Arc::clone(&wm);
|
||||
let mut wm = wm.lock();
|
||||
|
||||
let initial_state = State::from(wm.as_ref());
|
||||
@@ -346,12 +416,180 @@ where
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
if initial_monitor_count > attached_devices.len() {
|
||||
// Handle potential monitor removal with verification
|
||||
let attached_devices = if initial_monitor_count > attached_devices.len() {
|
||||
tracing::info!(
|
||||
"monitor count mismatch ({initial_monitor_count} vs {}), removing disconnected monitors",
|
||||
"potential monitor removal detected ({initial_monitor_count} vs {}), verifying in 3s",
|
||||
attached_devices.len()
|
||||
);
|
||||
|
||||
// Release locks before waiting
|
||||
drop(wm);
|
||||
drop(monitor_cache);
|
||||
|
||||
// Wait 3 seconds for display state to stabilize
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
|
||||
// Re-query the Win32 display APIs
|
||||
let re_queried_devices = match attached_display_devices(display_provider) {
|
||||
Ok(devices) => devices,
|
||||
Err(e) => {
|
||||
tracing::error!("failed to re-query display devices: {}", e);
|
||||
continue 'receiver;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
"after verification: wm had {} monitors, initial query found {}, re-query found {}",
|
||||
initial_monitor_count,
|
||||
attached_devices.len(),
|
||||
re_queried_devices.len()
|
||||
);
|
||||
|
||||
// If monitors are back, the removal was transient (spurious event)
|
||||
// Still try to restore state since windows might have been minimized
|
||||
if re_queried_devices.len() >= initial_monitor_count {
|
||||
tracing::info!(
|
||||
"monitor removal was transient (spurious event), attempting state restoration. Initial: {}, Re-queried: {}",
|
||||
initial_monitor_count,
|
||||
re_queried_devices.len()
|
||||
);
|
||||
|
||||
// Re-acquire locks for state restoration
|
||||
wm = wm_arc.lock();
|
||||
|
||||
// Update Win32 data for all monitors
|
||||
for monitor in wm.monitors_mut() {
|
||||
for attached in &re_queried_devices {
|
||||
let serial_number_ids_match =
|
||||
if let (Some(attached_snid), Some(m_snid)) =
|
||||
(&attached.serial_number_id, &monitor.serial_number_id)
|
||||
{
|
||||
attached_snid.eq(m_snid)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if serial_number_ids_match
|
||||
|| attached.device_id.eq(&monitor.device_id)
|
||||
{
|
||||
monitor.id = attached.id;
|
||||
monitor.device = attached.device.clone();
|
||||
monitor.device_id = attached.device_id.clone();
|
||||
monitor.serial_number_id = attached.serial_number_id.clone();
|
||||
monitor.name = attached.name.clone();
|
||||
monitor.size = attached.size;
|
||||
monitor.work_area_size = attached.work_area_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to restore windows that might have been minimized
|
||||
let offset = wm.work_area_offset;
|
||||
for monitor in wm.monitors_mut() {
|
||||
let focused_workspace_idx = monitor.focused_workspace_idx();
|
||||
|
||||
for (idx, workspace) in monitor.workspaces_mut().iter_mut().enumerate()
|
||||
{
|
||||
let is_focused_workspace = idx == focused_workspace_idx;
|
||||
|
||||
if is_focused_workspace {
|
||||
// Restore containers
|
||||
for container in workspace.containers_mut() {
|
||||
if let Some(window) = container.focused_window()
|
||||
&& WindowsApi::is_window(window.hwnd)
|
||||
{
|
||||
tracing::debug!(
|
||||
"restoring window after transient removal: {}",
|
||||
window.hwnd
|
||||
);
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
} else if let Some(window) = container.focused_window() {
|
||||
tracing::debug!(
|
||||
"skipping restore of invalid window: {}",
|
||||
window.hwnd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore maximized window
|
||||
if let Some(window) = &workspace.maximized_window
|
||||
&& WindowsApi::is_window(window.hwnd)
|
||||
{
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
|
||||
// Restore monocle container
|
||||
if let Some(container) = &workspace.monocle_container
|
||||
&& let Some(window) = container.focused_window()
|
||||
&& WindowsApi::is_window(window.hwnd)
|
||||
{
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
|
||||
// Restore floating windows
|
||||
for window in workspace.floating_windows() {
|
||||
if WindowsApi::is_window(window.hwnd) {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
monitor.update_focused_workspace(offset)?;
|
||||
}
|
||||
|
||||
border_manager::send_notification(None);
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
// If monitors are still missing, proceed with actual removal logic
|
||||
tracing::info!(
|
||||
"verified monitor removal ({initial_monitor_count} vs {}), removing disconnected monitors",
|
||||
re_queried_devices.len()
|
||||
);
|
||||
|
||||
// Re-acquire locks for removal processing
|
||||
wm = wm_arc.lock();
|
||||
monitor_cache = MONITOR_CACHE
|
||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
.lock();
|
||||
|
||||
// Make sure that in our state any attached displays have the latest Win32 data
|
||||
// We must do this again because we dropped the lock and are working with new data
|
||||
for monitor in wm.monitors_mut() {
|
||||
for attached in &re_queried_devices {
|
||||
let serial_number_ids_match =
|
||||
if let (Some(attached_snid), Some(m_snid)) =
|
||||
(&attached.serial_number_id, &monitor.serial_number_id)
|
||||
{
|
||||
attached_snid.eq(m_snid)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if serial_number_ids_match || attached.device_id.eq(&monitor.device_id)
|
||||
{
|
||||
monitor.id = attached.id;
|
||||
monitor.device = attached.device.clone();
|
||||
monitor.device_id = attached.device_id.clone();
|
||||
monitor.serial_number_id = attached.serial_number_id.clone();
|
||||
monitor.name = attached.name.clone();
|
||||
monitor.size = attached.size;
|
||||
monitor.work_area_size = attached.work_area_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use re-queried devices for remaining logic
|
||||
re_queried_devices
|
||||
} else {
|
||||
attached_devices
|
||||
};
|
||||
|
||||
if initial_monitor_count > attached_devices.len() {
|
||||
tracing::info!("removing disconnected monitors");
|
||||
|
||||
// Windows to remove from `known_hwnds`
|
||||
let mut windows_to_remove = Vec::new();
|
||||
|
||||
@@ -584,7 +822,9 @@ where
|
||||
}
|
||||
|
||||
if is_focused_workspace {
|
||||
if let Some(window) = container.focused_window() {
|
||||
if let Some(window) = container.focused_window()
|
||||
&& WindowsApi::is_window(window.hwnd)
|
||||
{
|
||||
tracing::debug!(
|
||||
"restoring window: {}",
|
||||
window.hwnd
|
||||
@@ -596,7 +836,9 @@ where
|
||||
// first window and show that one
|
||||
container.focus_window(0);
|
||||
|
||||
if let Some(window) = container.focused_window() {
|
||||
if let Some(window) = container.focused_window()
|
||||
&& WindowsApi::is_window(window.hwnd)
|
||||
{
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
@@ -617,7 +859,9 @@ where
|
||||
|| known_hwnds.contains_key(&window.hwnd)
|
||||
{
|
||||
workspace.maximized_window = None;
|
||||
} else if is_focused_workspace {
|
||||
} else if is_focused_workspace
|
||||
&& WindowsApi::is_window(window.hwnd)
|
||||
{
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
@@ -631,7 +875,9 @@ where
|
||||
if container.windows().is_empty() {
|
||||
workspace.monocle_container = None;
|
||||
} else if is_focused_workspace {
|
||||
if let Some(window) = container.focused_window() {
|
||||
if let Some(window) = container.focused_window()
|
||||
&& WindowsApi::is_window(window.hwnd)
|
||||
{
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
} else {
|
||||
// If the focused window was moved or removed by
|
||||
@@ -639,7 +885,9 @@ where
|
||||
// first window and show that one
|
||||
container.focus_window(0);
|
||||
|
||||
if let Some(window) = container.focused_window() {
|
||||
if let Some(window) = container.focused_window()
|
||||
&& WindowsApi::is_window(window.hwnd)
|
||||
{
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
@@ -653,7 +901,9 @@ where
|
||||
|
||||
if is_focused_workspace {
|
||||
for window in workspace.floating_windows() {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
if WindowsApi::is_window(window.hwnd) {
|
||||
WindowsApi::restore_window(window.hwnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
use crate::GlobalState;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::INITIAL_CONFIGURATION_LOADED;
|
||||
@@ -40,7 +39,6 @@ use crate::SESSION_FLOATING_APPLICATIONS;
|
||||
use crate::SUBSCRIPTION_PIPES;
|
||||
use crate::SUBSCRIPTION_SOCKET_OPTIONS;
|
||||
use crate::SUBSCRIPTION_SOCKETS;
|
||||
use crate::State;
|
||||
use crate::TCP_CONNECTIONS;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WINDOWS_11;
|
||||
@@ -62,9 +60,11 @@ use crate::core::Axis;
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::FocusFollowsMouseImplementation;
|
||||
use crate::core::Layout;
|
||||
use crate::core::LayoutOptions;
|
||||
use crate::core::MoveBehaviour;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
use crate::core::ScrollingLayoutOptions;
|
||||
use crate::core::Sizing;
|
||||
use crate::core::SocketMessage;
|
||||
use crate::core::StateQuery;
|
||||
@@ -74,19 +74,19 @@ use crate::core::config_generation::IdWithIdentifier;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use crate::default_layout::ScrollingLayoutOptions;
|
||||
use crate::monitor::MonitorInformation;
|
||||
use crate::notify_subscribers;
|
||||
use crate::stackbar_manager;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
|
||||
use crate::stackbar_manager::STACKBAR_FONT_SIZE;
|
||||
use crate::state;
|
||||
use crate::state::GlobalState;
|
||||
use crate::state::State;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::theme_manager;
|
||||
use crate::transparency_manager;
|
||||
use crate::window::RuleDebug;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager;
|
||||
use crate::window_manager::WindowManager;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent_listener;
|
||||
@@ -213,6 +213,7 @@ impl WindowManager {
|
||||
let mut force_update_borders = false;
|
||||
match message {
|
||||
SocketMessage::Promote => self.promote_container_to_front()?,
|
||||
SocketMessage::PromoteSwap => self.promote_container_swap()?,
|
||||
SocketMessage::PromoteFocus => self.promote_focus_to_front()?,
|
||||
SocketMessage::PromoteWindow(direction) => {
|
||||
self.focus_container_in_direction(direction)?;
|
||||
@@ -303,6 +304,28 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::PreselectDirection(direction) => {
|
||||
let focused_workspace = self.focused_workspace()?;
|
||||
let mut update = false;
|
||||
|
||||
if focused_workspace.preselected_container_idx.is_some() {
|
||||
tracing::warn!(
|
||||
"ignoring command as this workspace already has a direction preselect set"
|
||||
);
|
||||
} else if matches!(focused_workspace.layer, WorkspaceLayer::Tiling) {
|
||||
self.preselect_container_in_direction(direction)?;
|
||||
update = true;
|
||||
}
|
||||
|
||||
if update {
|
||||
self.focused_workspace_mut()?.update()?;
|
||||
}
|
||||
}
|
||||
SocketMessage::CancelPreselect => {
|
||||
let focused_workspace = self.focused_workspace_mut()?;
|
||||
focused_workspace.cancel_preselect();
|
||||
focused_workspace.update()?;
|
||||
}
|
||||
SocketMessage::MoveWindow(direction) => {
|
||||
let focused_workspace = self.focused_workspace()?;
|
||||
match focused_workspace.layer {
|
||||
@@ -923,6 +946,9 @@ impl WindowManager {
|
||||
columns: count.into(),
|
||||
center_focused_column: Default::default(),
|
||||
}),
|
||||
grid: None,
|
||||
column_ratios: None,
|
||||
row_ratios: None,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -931,6 +957,29 @@ impl WindowManager {
|
||||
}
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
||||
SocketMessage::LayoutRatios(ref columns, ref rows) => {
|
||||
use crate::core::validate_ratios;
|
||||
|
||||
let focused_workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let mut options = focused_workspace.layout_options.unwrap_or(LayoutOptions {
|
||||
scrolling: None,
|
||||
grid: None,
|
||||
column_ratios: None,
|
||||
row_ratios: None,
|
||||
});
|
||||
|
||||
if let Some(cols) = columns {
|
||||
options.column_ratios = Some(validate_ratios(cols));
|
||||
}
|
||||
|
||||
if let Some(rws) = rows {
|
||||
options.row_ratios = Some(validate_ratios(rws));
|
||||
}
|
||||
|
||||
focused_workspace.layout_options = Some(options);
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::ChangeLayoutCustom(ref path) => {
|
||||
self.change_workspace_custom_layout(path)?;
|
||||
}
|
||||
@@ -1358,8 +1407,7 @@ impl WindowManager {
|
||||
self.set_workspace_name(monitor_idx, workspace_idx, name.to_string())?;
|
||||
}
|
||||
SocketMessage::State => {
|
||||
let state = match serde_json::to_string_pretty(&window_manager::State::from(&*self))
|
||||
{
|
||||
let state = match serde_json::to_string_pretty(&state::State::from(&*self)) {
|
||||
Ok(state) => state,
|
||||
Err(error) => error.to_string(),
|
||||
};
|
||||
@@ -2201,14 +2249,7 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
|
||||
SocketMessage::StaticConfigSchema => {
|
||||
#[cfg(feature = "schemars")]
|
||||
{
|
||||
let settings = schemars::r#gen::SchemaSettings::default().with(|s| {
|
||||
s.option_nullable = false;
|
||||
s.option_add_null_type = false;
|
||||
s.inline_subschemas = true;
|
||||
});
|
||||
|
||||
let generator = settings.into_generator();
|
||||
let socket_message = generator.into_root_schema_for::<StaticConfig>();
|
||||
let socket_message = schemars::schema_for!(SocketMessage);
|
||||
let schema = serde_json::to_string_pretty(&socket_message)?;
|
||||
|
||||
reply.write_all(schema.as_bytes())?;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
@@ -19,7 +20,6 @@ use crate::Layout;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::State;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::VirtualDesktopNotification;
|
||||
use crate::Window;
|
||||
@@ -28,7 +28,10 @@ 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;
|
||||
@@ -42,6 +45,26 @@ use crate::workspace::WorkspaceLayer;
|
||||
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 {
|
||||
@@ -243,18 +266,33 @@ impl WindowManager {
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::Minimize(_, window) => {
|
||||
let mut hide = false;
|
||||
// During transient display connection changes (e.g. monitor
|
||||
// briefly disconnecting and reconnecting), Windows may fire
|
||||
// SystemMinimizeStart for windows on the affected monitor.
|
||||
// We must not treat these OS-initiated minimizes as user
|
||||
// actions, otherwise the window gets removed from the
|
||||
// workspace and the reconciliator cannot restore it.
|
||||
if crate::monitor_reconciliator::display_change_in_progress(
|
||||
std::time::Duration::from_secs(10),
|
||||
) {
|
||||
tracing::debug!(
|
||||
"ignoring minimize during display connection change for hwnd: {}",
|
||||
window.hwnd
|
||||
);
|
||||
} else {
|
||||
let mut hide = false;
|
||||
|
||||
{
|
||||
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
if !programmatically_hidden_hwnds.contains(&window.hwnd) {
|
||||
hide = true;
|
||||
{
|
||||
let programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
|
||||
if !programmatically_hidden_hwnds.contains(&window.hwnd) {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hide {
|
||||
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
if hide {
|
||||
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::Hide(_, window) => {
|
||||
@@ -408,6 +446,24 @@ impl WindowManager {
|
||||
proceed = false;
|
||||
}
|
||||
|
||||
// after enforce_workspace_rules() has run, check if window exists in ANY workspace
|
||||
// to prevent duplication when workspace rules move windows across workspaces
|
||||
if proceed {
|
||||
let window_already_managed = self
|
||||
.monitors()
|
||||
.iter()
|
||||
.flat_map(|m| m.workspaces())
|
||||
.any(|ws| ws.contains_window(window.hwnd));
|
||||
|
||||
if window_already_managed {
|
||||
tracing::debug!(
|
||||
"skipping window addition, already managed after workspace rule enforcement"
|
||||
);
|
||||
|
||||
proceed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if proceed {
|
||||
let behaviour = self.window_management_behaviour(
|
||||
focused_monitor_idx,
|
||||
|
||||
@@ -183,6 +183,7 @@ 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
|
||||
|
||||
157
komorebi/src/splash.rs
Normal file
157
komorebi/src/splash.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use crate::DATA_DIR;
|
||||
use crate::License;
|
||||
use crate::PUBLIC_KEY;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose;
|
||||
use chrono::Duration;
|
||||
use chrono::TimeZone;
|
||||
use chrono::Utc;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::OptionExt;
|
||||
use ed25519_dalek::Verifier;
|
||||
use ed25519_dalek::VerifyingKey;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn mdm_enrollment() -> eyre::Result<(bool, Option<String>)> {
|
||||
let mut command = Command::new("dsregcmd");
|
||||
command.args(["/status"]);
|
||||
let stdout = command.output()?.stdout;
|
||||
let output = std::str::from_utf8(&stdout)?;
|
||||
if !output.contains("WorkspaceTenantName") {
|
||||
return Ok((false, None));
|
||||
}
|
||||
|
||||
let mut tenant = None;
|
||||
|
||||
for line in output.lines() {
|
||||
if line.contains("WorkspaceTenantName") {
|
||||
let line = line.trim().to_string();
|
||||
tenant = Some(
|
||||
line.trim_start_matches("WorkspaceTenantName : ")
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok((true, tenant))
|
||||
}
|
||||
|
||||
fn is_valid_payload(raw: &str, fresh: bool) -> eyre::Result<bool> {
|
||||
let mut validation_successful = false;
|
||||
|
||||
let payload = serde_json::from_str::<License>(raw)?;
|
||||
|
||||
let signature = ed25519_dalek::Signature::from_slice(
|
||||
general_purpose::STANDARD
|
||||
.decode(&payload.signature)?
|
||||
.as_slice(),
|
||||
)?;
|
||||
|
||||
let mut value: serde_json::Value = serde_json::from_str(raw)?;
|
||||
if let serde_json::Value::Object(ref mut map) = value {
|
||||
map.remove("signature");
|
||||
}
|
||||
|
||||
let message_to_verify = serde_json::to_string(&value)?;
|
||||
let verifying_key = VerifyingKey::from_bytes(&PUBLIC_KEY)?;
|
||||
|
||||
if verifying_key
|
||||
.verify(message_to_verify.as_bytes(), &signature)
|
||||
.is_ok()
|
||||
{
|
||||
if fresh {
|
||||
let timestamp = Utc
|
||||
.timestamp_opt(payload.timestamp, 0)
|
||||
.single()
|
||||
.ok_or_eyre("invalid timestamp")?;
|
||||
|
||||
let valid_duration = Utc::now() - Duration::minutes(5);
|
||||
|
||||
if timestamp <= valid_duration {
|
||||
tracing::debug!("individual commercial use license verification payload was stale");
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
if payload.has_valid_subscription
|
||||
&& let Some(current_end_period) = payload.current_end_period
|
||||
{
|
||||
let subscription_valid_until = Utc
|
||||
.timestamp_opt(current_end_period, 0)
|
||||
.single()
|
||||
.ok_or_eyre("invalid timestamp")?;
|
||||
|
||||
if Utc::now() <= subscription_valid_until {
|
||||
tracing::debug!(
|
||||
"individual commercial use license verification - subscription valid until: {subscription_valid_until}",
|
||||
);
|
||||
|
||||
validation_successful = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(validation_successful)
|
||||
}
|
||||
|
||||
pub enum ValidationFeedback {
|
||||
Successful(PathBuf),
|
||||
Unsuccessful(String),
|
||||
NoEmail,
|
||||
NoConnectivity,
|
||||
}
|
||||
|
||||
impl From<ValidationFeedback> for bool {
|
||||
fn from(value: ValidationFeedback) -> Self {
|
||||
match value {
|
||||
ValidationFeedback::Successful(_) => false,
|
||||
|
||||
ValidationFeedback::Unsuccessful(_)
|
||||
| ValidationFeedback::NoEmail
|
||||
| ValidationFeedback::NoConnectivity => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should() -> eyre::Result<ValidationFeedback> {
|
||||
let icul_validation = DATA_DIR.join("icul.validation");
|
||||
if icul_validation.exists() {
|
||||
tracing::debug!("found local individual commercial use license validation payload");
|
||||
let raw_payload = std::fs::read_to_string(&icul_validation)?;
|
||||
if is_valid_payload(&raw_payload, false)? {
|
||||
return Ok(ValidationFeedback::Successful(icul_validation));
|
||||
} else {
|
||||
std::fs::remove_file(&icul_validation)?;
|
||||
}
|
||||
}
|
||||
|
||||
let icul = DATA_DIR.join("icul");
|
||||
if !icul.exists() {
|
||||
return Ok(ValidationFeedback::NoEmail);
|
||||
}
|
||||
|
||||
let email = std::fs::read_to_string(icul)?;
|
||||
tracing::debug!("found individual commercial use license email: {}", email);
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = match client
|
||||
.get("https://kw-icul.lgug2z.com")
|
||||
.query(&[("email", email.trim())])
|
||||
.send()
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
return Ok(ValidationFeedback::NoConnectivity);
|
||||
}
|
||||
};
|
||||
|
||||
let raw_payload = response.text()?;
|
||||
if is_valid_payload(&raw_payload, true)? {
|
||||
std::fs::write(&icul_validation, &raw_payload)?;
|
||||
Ok(ValidationFeedback::Successful(icul_validation))
|
||||
} else {
|
||||
Ok(ValidationFeedback::Unsuccessful(raw_payload))
|
||||
}
|
||||
}
|
||||
308
komorebi/src/state.rs
Normal file
308
komorebi/src/state.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
use crate::BorderColours;
|
||||
use crate::BorderStyle;
|
||||
use crate::CURRENT_VIRTUAL_DESKTOP;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
|
||||
use crate::FocusFollowsMouseImplementation;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::HOME_DIR;
|
||||
use crate::HidingBehaviour;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::MoveBehaviour;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::OperationBehaviour;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::Rect;
|
||||
use crate::StackbarLabel;
|
||||
use crate::StackbarMode;
|
||||
use crate::TRANSPARENCY_BLACKLIST;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
use crate::WindowContainerBehaviour;
|
||||
use crate::WindowManager;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::config_generation::MatchingRule;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_LABEL;
|
||||
use crate::stackbar_manager::STACKBAR_MODE;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
|
||||
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::transparency_manager::TRANSPARENCY_ALPHA;
|
||||
use crate::transparency_manager::TRANSPARENCY_ENABLED;
|
||||
use crate::workspace::Workspace;
|
||||
use komorebi_themes::colour::Colour;
|
||||
use komorebi_themes::colour::Rgb;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct State {
|
||||
pub monitors: Ring<Monitor>,
|
||||
pub monitor_usr_idx_map: HashMap<usize, usize>,
|
||||
pub is_paused: bool,
|
||||
pub resize_delta: i32,
|
||||
pub new_window_behaviour: WindowContainerBehaviour,
|
||||
pub float_override: bool,
|
||||
pub cross_monitor_move_behaviour: MoveBehaviour,
|
||||
pub unmanaged_window_operation_behaviour: OperationBehaviour,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
pub mouse_follows_focus: bool,
|
||||
pub has_pending_raise_op: bool,
|
||||
pub virtual_desktop_id: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn has_been_modified(&self, wm: &WindowManager) -> bool {
|
||||
let new = Self::from(wm);
|
||||
|
||||
if self.monitors != new.monitors {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.is_paused != new.is_paused {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.new_window_behaviour != new.new_window_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.float_override != new.float_override {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.cross_monitor_move_behaviour != new.cross_monitor_move_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.unmanaged_window_operation_behaviour != new.unmanaged_window_operation_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.work_area_offset != new.work_area_offset {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.focus_follows_mouse != new.focus_follows_mouse {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.mouse_follows_focus != new.mouse_follows_focus {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.has_pending_raise_op != new.has_pending_raise_op {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct GlobalState {
|
||||
pub border_enabled: bool,
|
||||
pub border_colours: BorderColours,
|
||||
pub border_style: BorderStyle,
|
||||
pub border_offset: i32,
|
||||
pub border_width: i32,
|
||||
pub stackbar_mode: StackbarMode,
|
||||
pub stackbar_label: StackbarLabel,
|
||||
pub stackbar_focused_text_colour: Colour,
|
||||
pub stackbar_unfocused_text_colour: Colour,
|
||||
pub stackbar_tab_background_colour: Colour,
|
||||
pub stackbar_tab_width: i32,
|
||||
pub stackbar_height: i32,
|
||||
pub transparency_enabled: bool,
|
||||
pub transparency_alpha: u8,
|
||||
pub transparency_blacklist: Vec<MatchingRule>,
|
||||
pub remove_titlebars: bool,
|
||||
#[serde(alias = "float_identifiers")]
|
||||
pub ignore_identifiers: Vec<MatchingRule>,
|
||||
pub manage_identifiers: Vec<MatchingRule>,
|
||||
pub layered_whitelist: Vec<MatchingRule>,
|
||||
pub tray_and_multi_window_identifiers: Vec<MatchingRule>,
|
||||
pub name_change_on_launch_identifiers: Vec<MatchingRule>,
|
||||
pub monitor_index_preferences: HashMap<usize, Rect>,
|
||||
pub display_index_preferences: HashMap<usize, String>,
|
||||
pub ignored_duplicate_monitor_serial_ids: Vec<String>,
|
||||
pub workspace_rules: Vec<WorkspaceMatchingRule>,
|
||||
pub window_hiding_behaviour: HidingBehaviour,
|
||||
pub configuration_dir: PathBuf,
|
||||
pub data_dir: PathBuf,
|
||||
pub custom_ffm: bool,
|
||||
pub current_virtual_desktop_id: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Default for GlobalState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
border_enabled: border_manager::BORDER_ENABLED.load(Ordering::SeqCst),
|
||||
border_colours: BorderColours {
|
||||
single: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::FOCUSED.load(Ordering::SeqCst),
|
||||
))),
|
||||
stack: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::STACK.load(Ordering::SeqCst),
|
||||
))),
|
||||
monocle: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::MONOCLE.load(Ordering::SeqCst),
|
||||
))),
|
||||
floating: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::FLOATING.load(Ordering::SeqCst),
|
||||
))),
|
||||
unfocused: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::UNFOCUSED.load(Ordering::SeqCst),
|
||||
))),
|
||||
unfocused_locked: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::UNFOCUSED_LOCKED.load(Ordering::SeqCst),
|
||||
))),
|
||||
},
|
||||
border_style: STYLE.load(),
|
||||
border_offset: border_manager::BORDER_OFFSET.load(Ordering::SeqCst),
|
||||
border_width: border_manager::BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
stackbar_mode: STACKBAR_MODE.load(),
|
||||
stackbar_label: STACKBAR_LABEL.load(),
|
||||
stackbar_focused_text_colour: Colour::Rgb(Rgb::from(
|
||||
STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
|
||||
)),
|
||||
stackbar_unfocused_text_colour: Colour::Rgb(Rgb::from(
|
||||
STACKBAR_UNFOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
|
||||
)),
|
||||
stackbar_tab_background_colour: Colour::Rgb(Rgb::from(
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.load(Ordering::SeqCst),
|
||||
)),
|
||||
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
|
||||
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
|
||||
transparency_enabled: TRANSPARENCY_ENABLED.load(Ordering::SeqCst),
|
||||
transparency_alpha: TRANSPARENCY_ALPHA.load(Ordering::SeqCst),
|
||||
transparency_blacklist: TRANSPARENCY_BLACKLIST.lock().clone(),
|
||||
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
|
||||
ignore_identifiers: IGNORE_IDENTIFIERS.lock().clone(),
|
||||
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
|
||||
layered_whitelist: LAYERED_WHITELIST.lock().clone(),
|
||||
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
|
||||
name_change_on_launch_identifiers: OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
|
||||
monitor_index_preferences: MONITOR_INDEX_PREFERENCES.lock().clone(),
|
||||
display_index_preferences: DISPLAY_INDEX_PREFERENCES.read().clone(),
|
||||
ignored_duplicate_monitor_serial_ids: DUPLICATE_MONITOR_SERIAL_IDS.read().clone(),
|
||||
workspace_rules: WORKSPACE_MATCHING_RULES.lock().clone(),
|
||||
window_hiding_behaviour: *HIDING_BEHAVIOUR.lock(),
|
||||
configuration_dir: HOME_DIR.clone(),
|
||||
data_dir: DATA_DIR.clone(),
|
||||
custom_ffm: CUSTOM_FFM.load(Ordering::SeqCst),
|
||||
current_virtual_desktop_id: CURRENT_VIRTUAL_DESKTOP.lock().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&WindowManager> for State {
|
||||
fn from(wm: &WindowManager) -> Self {
|
||||
// This is used to remove any information that doesn't need to be passed on to subscribers
|
||||
// or to be shown with the `komorebic state` command. Currently it is only removing the
|
||||
// `workspace_config` field from every workspace, but more stripping can be added later if
|
||||
// needed.
|
||||
let mut stripped_monitors = Ring::default();
|
||||
*stripped_monitors.elements_mut() = wm
|
||||
.monitors()
|
||||
.iter()
|
||||
.map(|monitor| Monitor {
|
||||
id: monitor.id,
|
||||
name: monitor.name.clone(),
|
||||
device: monitor.device.clone(),
|
||||
device_id: monitor.device_id.clone(),
|
||||
serial_number_id: monitor.serial_number_id.clone(),
|
||||
size: monitor.size,
|
||||
work_area_size: monitor.work_area_size,
|
||||
work_area_offset: monitor.work_area_offset,
|
||||
window_based_work_area_offset: monitor.window_based_work_area_offset,
|
||||
window_based_work_area_offset_limit: monitor.window_based_work_area_offset_limit,
|
||||
workspaces: {
|
||||
let mut ws = Ring::default();
|
||||
*ws.elements_mut() = monitor
|
||||
.workspaces()
|
||||
.iter()
|
||||
.map(|workspace| Workspace {
|
||||
name: workspace.name.clone(),
|
||||
containers: workspace.containers.clone(),
|
||||
monocle_container: workspace.monocle_container.clone(),
|
||||
monocle_container_restore_idx: workspace.monocle_container_restore_idx,
|
||||
maximized_window: workspace.maximized_window,
|
||||
maximized_window_restore_idx: workspace.maximized_window_restore_idx,
|
||||
floating_windows: workspace.floating_windows.clone(),
|
||||
layout: workspace.layout.clone(),
|
||||
layout_options: workspace.layout_options,
|
||||
layout_rules: workspace.layout_rules.clone(),
|
||||
layout_flip: workspace.layout_flip,
|
||||
workspace_padding: workspace.workspace_padding,
|
||||
container_padding: workspace.container_padding,
|
||||
latest_layout: workspace.latest_layout.clone(),
|
||||
resize_dimensions: workspace.resize_dimensions.clone(),
|
||||
tile: workspace.tile,
|
||||
work_area_offset: workspace.work_area_offset,
|
||||
apply_window_based_work_area_offset: workspace
|
||||
.apply_window_based_work_area_offset,
|
||||
window_container_behaviour: workspace.window_container_behaviour,
|
||||
window_container_behaviour_rules: workspace
|
||||
.window_container_behaviour_rules
|
||||
.clone(),
|
||||
float_override: workspace.float_override,
|
||||
layer: workspace.layer,
|
||||
floating_layer_behaviour: workspace.floating_layer_behaviour,
|
||||
globals: workspace.globals,
|
||||
wallpaper: workspace.wallpaper.clone(),
|
||||
workspace_config: None,
|
||||
preselected_container_idx: None,
|
||||
promotion_swap_container_idx: None,
|
||||
})
|
||||
.collect::<VecDeque<_>>();
|
||||
ws.focus(monitor.workspaces.focused_idx());
|
||||
ws
|
||||
},
|
||||
last_focused_workspace: monitor.last_focused_workspace,
|
||||
workspace_names: monitor.workspace_names.clone(),
|
||||
container_padding: monitor.container_padding,
|
||||
workspace_padding: monitor.workspace_padding,
|
||||
wallpaper: monitor.wallpaper.clone(),
|
||||
floating_layer_behaviour: monitor.floating_layer_behaviour,
|
||||
})
|
||||
.collect::<VecDeque<_>>();
|
||||
stripped_monitors.focus(wm.monitors.focused_idx());
|
||||
|
||||
Self {
|
||||
monitors: stripped_monitors,
|
||||
monitor_usr_idx_map: wm.monitor_usr_idx_map.clone(),
|
||||
is_paused: wm.is_paused,
|
||||
work_area_offset: wm.work_area_offset,
|
||||
resize_delta: wm.resize_delta,
|
||||
new_window_behaviour: wm.window_management_behaviour.current_behaviour,
|
||||
float_override: wm.window_management_behaviour.float_override,
|
||||
cross_monitor_move_behaviour: wm.cross_monitor_move_behaviour,
|
||||
focus_follows_mouse: wm.focus_follows_mouse,
|
||||
mouse_follows_focus: wm.mouse_follows_focus,
|
||||
has_pending_raise_op: wm.has_pending_raise_op,
|
||||
unmanaged_window_operation_behaviour: wm.unmanaged_window_operation_behaviour,
|
||||
virtual_desktop_id: wm.virtual_desktop_id.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ use crate::Axis;
|
||||
use crate::CrossBoundaryBehaviour;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DEFAULT_CONTAINER_PADDING;
|
||||
use crate::DEFAULT_MOUSE_FOLLOWS_FOCUS;
|
||||
use crate::DEFAULT_RESIZE_DELTA;
|
||||
use crate::DEFAULT_WORKSPACE_PADDING;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::FLOATING_APPLICATIONS;
|
||||
@@ -51,6 +53,7 @@ use crate::core::DefaultLayout;
|
||||
use crate::core::FocusFollowsMouseImplementation;
|
||||
use crate::core::HidingBehaviour;
|
||||
use crate::core::Layout;
|
||||
use crate::core::LayoutOptions;
|
||||
use crate::core::MoveBehaviour;
|
||||
use crate::core::OperationBehaviour;
|
||||
use crate::core::Rect;
|
||||
@@ -65,7 +68,6 @@ use crate::core::config_generation::ApplicationOptions;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::config_generation::MatchingStrategy;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use crate::monitor;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::monitor_reconciliator;
|
||||
@@ -108,6 +110,7 @@ use uds_windows::UnixStream;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Border colours for different container states
|
||||
pub struct BorderColours {
|
||||
/// Border colour when the container contains a single window
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -131,54 +134,69 @@ pub struct BorderColours {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Theme options
|
||||
pub struct ThemeOptions {
|
||||
/// Specify Light or Dark variant for theme generation (default: Dark)
|
||||
/// Specify Light or Dark variant for theme generation
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::ThemeVariant::Dark)))]
|
||||
pub theme_variant: Option<komorebi_themes::ThemeVariant>,
|
||||
/// Border colour when the container contains a single window (default: Base0D)
|
||||
/// Border colour when the container contains a single window
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base0D)))]
|
||||
pub single_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container contains multiple windows (default: Base0B)
|
||||
/// Border colour when the container contains multiple windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base0B)))]
|
||||
pub stack_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is in monocle mode (default: Base0F)
|
||||
/// Border colour when the container is in monocle mode
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base0F)))]
|
||||
pub monocle_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the window is floating (default: Base09)
|
||||
/// Border colour when the window is floating
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base09)))]
|
||||
pub floating_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is unfocused (default: Base01)
|
||||
/// Border colour when the container is unfocused
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base01)))]
|
||||
pub unfocused_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is unfocused and locked (default: Base08)
|
||||
/// Border colour when the container is unfocused and locked
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base08)))]
|
||||
pub unfocused_locked_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar focused tab text colour (default: Base0B)
|
||||
/// Stackbar focused tab text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base0B)))]
|
||||
pub stackbar_focused_text: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar unfocused tab text colour (default: Base05)
|
||||
/// Stackbar unfocused tab text colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base05)))]
|
||||
pub stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar tab background colour (default: Base01)
|
||||
/// Stackbar tab background colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base01)))]
|
||||
pub stackbar_background: Option<komorebi_themes::Base16Value>,
|
||||
/// Komorebi status bar accent (default: Base0D)
|
||||
/// Komorebi status bar accent
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::Base16Value::Base0D)))]
|
||||
pub bar_accent: Option<komorebi_themes::Base16Value>,
|
||||
}
|
||||
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Wallpaper configuration
|
||||
pub struct Wallpaper {
|
||||
/// Path to the wallpaper image file
|
||||
#[serde_as(as = "ResolvedPathBuf")]
|
||||
pub path: PathBuf,
|
||||
/// Generate and apply Base16 theme for this wallpaper (default: true)
|
||||
/// Generate and apply Base16 theme for this wallpaper
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||
pub generate_theme: Option<bool>,
|
||||
/// Specify Light or Dark variant for theme generation (default: Dark)
|
||||
/// Specify Light or Dark variant for theme generation
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = komorebi_themes::ThemeVariant::Dark)))]
|
||||
pub theme_options: Option<ThemeOptions>,
|
||||
}
|
||||
|
||||
@@ -186,23 +204,27 @@ pub struct Wallpaper {
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Workspace configuration
|
||||
pub struct WorkspaceConfig {
|
||||
/// Name
|
||||
pub name: String,
|
||||
/// Layout (default: BSP)
|
||||
/// Layout
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = DefaultLayout::BSP)))]
|
||||
pub layout: Option<DefaultLayout>,
|
||||
/// Layout-specific options (default: None)
|
||||
/// Layout-specific options
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_options: Option<LayoutOptions>,
|
||||
/// END OF LIFE FEATURE: Custom Layout (default: None)
|
||||
/// END OF LIFE FEATURE: Custom Layout
|
||||
#[deprecated(note = "End of life feature")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde_as(as = "Option<ResolvedPathBuf>")]
|
||||
pub custom_layout: Option<PathBuf>,
|
||||
/// Layout rules in the format of threshold => layout (default: None)
|
||||
/// Layout rules in the format of threshold => layout
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
|
||||
/// END OF LIFE FEATURE: Custom layout rules (default: None)
|
||||
/// END OF LIFE FEATURE: Custom layout rules
|
||||
#[deprecated(note = "End of life feature")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(deserialize_with = "resolve_option_hashmap_usize_path", default)]
|
||||
pub custom_layout_rules: Option<HashMap<usize, PathBuf>>,
|
||||
@@ -218,29 +240,34 @@ pub struct WorkspaceConfig {
|
||||
/// Permanent workspace application rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub workspace_rules: Option<Vec<MatchingRule>>,
|
||||
/// Workspace specific work area offset (default: None)
|
||||
/// Workspace specific work area offset
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub work_area_offset: Option<Rect>,
|
||||
/// Apply this monitor's window-based work area offset (default: true)
|
||||
/// Apply this monitor's window-based work area offset
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||
pub apply_window_based_work_area_offset: Option<bool>,
|
||||
/// Determine what happens when a new window is opened (default: Create)
|
||||
/// Determine what happens when a new window is opened
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = WindowContainerBehaviour::Create)))]
|
||||
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
/// Window container behaviour rules in the format of threshold => behaviour (default: None)
|
||||
/// Window container behaviour rules in the format of threshold => behaviour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_container_behaviour_rules: Option<HashMap<usize, WindowContainerBehaviour>>,
|
||||
/// Enable or disable float override, which makes it so every new window opens in floating mode (default: false)
|
||||
/// Enable or disable float override, which makes it so every new window opens in floating mode
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
|
||||
pub float_override: Option<bool>,
|
||||
/// Enable or disable tiling for the workspace (default: true)
|
||||
/// Enable or disable tiling for the workspace
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
|
||||
pub tile: Option<bool>,
|
||||
/// Specify an axis on which to flip the selected layout (default: None)
|
||||
/// Specify an axis on which to flip the selected layout
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_flip: Option<Axis>,
|
||||
/// Determine what happens to a new window when the Floating workspace layer is active (default: Tile)
|
||||
/// Determine what happens to a new window when the Floating workspace layer is active
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = FloatingLayerBehaviour::Tile)))]
|
||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||
/// Specify a wallpaper for this workspace
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -298,12 +325,20 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
Layout::Custom(_) => None,
|
||||
})
|
||||
.flatten(),
|
||||
layout_options: value.layout_options,
|
||||
layout_options: {
|
||||
tracing::debug!(
|
||||
"Parsing workspace config - layout_options: {:?}",
|
||||
value.layout_options
|
||||
);
|
||||
value.layout_options
|
||||
},
|
||||
#[allow(deprecated)]
|
||||
custom_layout: value
|
||||
.workspace_config
|
||||
.as_ref()
|
||||
.and_then(|c| c.custom_layout.clone()),
|
||||
layout_rules,
|
||||
#[allow(deprecated)]
|
||||
custom_layout_rules: value
|
||||
.workspace_config
|
||||
.as_ref()
|
||||
@@ -333,17 +368,19 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Monitor configuration
|
||||
pub struct MonitorConfig {
|
||||
/// Workspace configurations
|
||||
pub workspaces: Vec<WorkspaceConfig>,
|
||||
/// Monitor-specific work area offset (default: None)
|
||||
/// Monitor-specific work area offset
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub work_area_offset: Option<Rect>,
|
||||
/// Window based work area offset (default: None)
|
||||
/// Window based work area offset
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_based_work_area_offset: Option<Rect>,
|
||||
/// Open window limit after which the window based work area offset will no longer be applied (default: 1)
|
||||
/// Open window limit after which the window based work area offset will no longer be applied
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = 1)))]
|
||||
pub window_based_work_area_offset_limit: Option<isize>,
|
||||
/// Container padding (default: global)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -354,8 +391,9 @@ pub struct MonitorConfig {
|
||||
/// Specify a wallpaper for this monitor
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub wallpaper: Option<Wallpaper>,
|
||||
/// Determine what happens to a new window when the Floating workspace layer is active (default: Tile)
|
||||
/// Determine what happens to a new window when the Floating workspace layer is active
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = FloatingLayerBehaviour::Tile)))]
|
||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||
}
|
||||
|
||||
@@ -402,19 +440,21 @@ impl From<&Monitor> for MonitorConfig {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Path(s) to application-specific configuration file(s)
|
||||
pub enum AppSpecificConfigurationPath {
|
||||
/// A single applications.json file
|
||||
/// A single `applications.json` file
|
||||
Single(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
/// Multiple applications.json files
|
||||
/// Multiple `applications.json` files
|
||||
Multiple(#[serde_as(as = "Vec<ResolvedPathBuf>")] Vec<PathBuf>),
|
||||
}
|
||||
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.39`
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.40`
|
||||
pub struct StaticConfig {
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[deprecated(note = "No longer required")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub invisible_borders: Option<Rect>,
|
||||
/// DISCOURAGED: Minimum width for a window to be eligible for tiling
|
||||
@@ -423,101 +463,122 @@ pub struct StaticConfig {
|
||||
/// DISCOURAGED: Minimum height for a window to be eligible for tiling
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum_window_height: Option<i32>,
|
||||
/// Delta to resize windows by (default 50)
|
||||
/// Delta to resize windows by
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_RESIZE_DELTA)))]
|
||||
pub resize_delta: Option<i32>,
|
||||
/// Determine what happens when a new window is opened (default: Create)
|
||||
/// Determine what happens when a new window is opened
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = WindowContainerBehaviour::Create)))]
|
||||
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
/// Enable or disable float override, which makes it so every new window opens in floating mode
|
||||
/// (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
|
||||
pub float_override: Option<bool>,
|
||||
/// Determines what happens on a new window when on the `FloatingLayer`
|
||||
/// (default: Tile)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = FloatingLayerBehaviour::Tile)))]
|
||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||
/// Determines the placement of a new window when toggling to float (default: CenterAndResize)
|
||||
/// Determines the placement of a new window when toggling to float
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Placement::CenterAndResize)))]
|
||||
pub toggle_float_placement: Option<Placement>,
|
||||
/// Determines the `Placement` to be used when spawning a window on the floating layer with the
|
||||
/// `FloatingLayerBehaviour` set to `FloatingLayerBehaviour::Float` (default: Center)
|
||||
/// `FloatingLayerBehaviour` set to `FloatingLayerBehaviour::Float`
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Placement::Center)))]
|
||||
pub floating_layer_placement: Option<Placement>,
|
||||
/// Determines the `Placement` to be used when spawning a window with float override active
|
||||
/// (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_override_placement: Option<Placement>,
|
||||
/// Determines the `Placement` to be used when spawning a window that matches a
|
||||
/// 'floating_applications' rule (default: None)
|
||||
/// `floating_applications` rule
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_rule_placement: Option<Placement>,
|
||||
/// Determine what happens when a window is moved across a monitor boundary (default: Swap)
|
||||
/// Determine what happens when a window is moved across a monitor boundary
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = MoveBehaviour::Swap)))]
|
||||
pub cross_monitor_move_behaviour: Option<MoveBehaviour>,
|
||||
/// Determine what happens when an action is called on a window at a monitor boundary (default: Monitor)
|
||||
/// Determine what happens when an action is called on a window at a monitor boundary
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = CrossBoundaryBehaviour::Monitor)))]
|
||||
pub cross_boundary_behaviour: Option<CrossBoundaryBehaviour>,
|
||||
/// Determine what happens when commands are sent while an unmanaged window is in the foreground (default: Op)
|
||||
/// Determine what happens when commands are sent while an unmanaged window is in the foreground
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = OperationBehaviour::Op)))]
|
||||
pub unmanaged_window_operation_behaviour: Option<OperationBehaviour>,
|
||||
/// END OF LIFE FEATURE: Use https://github.com/LGUG2Z/masir instead
|
||||
#[deprecated(
|
||||
note = "End of life feature, use [masir](https://github.com/LGUG2Z/masir) instead"
|
||||
)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
/// Enable or disable mouse follows focus (default: true)
|
||||
/// Enable or disable mouse follows focus
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_MOUSE_FOLLOWS_FOCUS)))]
|
||||
pub mouse_follows_focus: Option<bool>,
|
||||
/// Path to applications.json from komorebi-application-specific-configurations (default: None)
|
||||
/// Path to applications.json from komorebi-application-specific-configurations
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub app_specific_configuration_path: Option<AppSpecificConfigurationPath>,
|
||||
/// Width of the window border (default: 8)
|
||||
/// Width of window borders
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(alias = "active_window_border_width")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = border_manager::BORDER_WIDTH)))]
|
||||
pub border_width: Option<i32>,
|
||||
/// Offset of the window border (default: -1)
|
||||
/// Offset of window borders
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(alias = "active_window_border_offset")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = border_manager::BORDER_OFFSET)))]
|
||||
pub border_offset: Option<i32>,
|
||||
/// Display an active window border (default: true)
|
||||
/// Display window borders
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(alias = "active_window_border")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = border_manager::BORDER_ENABLED)))]
|
||||
pub border: Option<bool>,
|
||||
/// Active window border colours for different container types
|
||||
/// Window border colours for different container types (has no effect if [`theme`] is defined)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(alias = "active_window_border_colours")]
|
||||
pub border_colours: Option<BorderColours>,
|
||||
/// Active window border style (default: System)
|
||||
/// Window border style
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(alias = "active_window_border_style")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = BorderStyle::System)))]
|
||||
pub border_style: Option<BorderStyle>,
|
||||
/// DEPRECATED from v0.1.31: no longer required
|
||||
#[deprecated(note = "No longer required")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border_z_order: Option<ZOrder>,
|
||||
/// Active window border implementation (default: Komorebi)
|
||||
/// Window border implementation
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = BorderImplementation::Komorebi)))]
|
||||
pub border_implementation: Option<BorderImplementation>,
|
||||
/// Add transparency to unfocused windows (default: false)
|
||||
/// Add transparency to unfocused windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = transparency_manager::TRANSPARENCY_ENABLED)))]
|
||||
pub transparency: Option<bool>,
|
||||
/// Alpha value for unfocused window transparency [[0-255]] (default: 200)
|
||||
/// Alpha value for unfocused window transparency [[0-255]]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = transparency_manager::TRANSPARENCY_ALPHA)))]
|
||||
pub transparency_alpha: Option<u8>,
|
||||
/// Individual window transparency ignore rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transparency_ignore_rules: Option<Vec<MatchingRule>>,
|
||||
/// Global default workspace padding (default: 10)
|
||||
/// Global default workspace padding
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_WORKSPACE_PADDING)))]
|
||||
pub default_workspace_padding: Option<i32>,
|
||||
/// Global default container padding (default: 10)
|
||||
/// Global default container padding
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = DEFAULT_CONTAINER_PADDING)))]
|
||||
pub default_container_padding: Option<i32>,
|
||||
/// Monitor and workspace configurations
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitors: Option<Vec<MonitorConfig>>,
|
||||
/// Which Windows signal to use when hiding windows (default: Cloak)
|
||||
/// Which Windows signal to use when hiding windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = HidingBehaviour::Cloak)))]
|
||||
pub window_hiding_behaviour: Option<HidingBehaviour>,
|
||||
/// Global work area (space used for tiling) offset (default: None)
|
||||
/// Global work area (space used for tiling) offset
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub global_work_area_offset: Option<Rect>,
|
||||
/// Individual window floating rules
|
||||
@@ -536,13 +597,13 @@ pub struct StaticConfig {
|
||||
/// Identify tray and multi-window applications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tray_and_multi_window_applications: Option<Vec<MatchingRule>>,
|
||||
/// Identify applications that have the WS_EX_LAYERED extended window style
|
||||
/// Identify applications that have the `WS_EX_LAYERED` extended window style
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layered_applications: Option<Vec<MatchingRule>>,
|
||||
/// Identify applications that send EVENT_OBJECT_NAMECHANGE on launch (very rare)
|
||||
/// Identify applications that send `EVENT_OBJECT_NAMECHANGE` on launch (very rare)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_name_change_applications: Option<Vec<MatchingRule>>,
|
||||
/// Do not process EVENT_OBJECT_NAMECHANGE events as Show events for identified applications matching these title regexes
|
||||
/// Do not process `EVENT_OBJECT_NAMECHANGE` events as Show events for identified applications matching these title regexes
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_name_change_title_ignore_list: Option<Vec<String>>,
|
||||
/// Set monitor index preferences
|
||||
@@ -558,13 +619,16 @@ pub struct StaticConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub animation: Option<AnimationsConfig>,
|
||||
/// Theme configuration options
|
||||
///
|
||||
/// If a theme is specified, `border_colours` will have no effect
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub theme: Option<KomorebiTheme>,
|
||||
/// Identify applications which are slow to send initial event notifications
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slow_application_identifiers: Option<Vec<MatchingRule>>,
|
||||
/// How long to wait when compensating for slow applications, in milliseconds (default: 20)
|
||||
/// How long to wait when compensating for slow applications, in milliseconds
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = SLOW_APPLICATION_COMPENSATION_TIME)))]
|
||||
pub slow_application_compensation_time: Option<u64>,
|
||||
/// Komorebi status bar configuration files for multiple instances on different monitors
|
||||
// this option is a little special because it is only consumed by komorebic
|
||||
@@ -577,137 +641,34 @@ pub struct StaticConfig {
|
||||
/// Aspect ratio to resize with when toggling floating mode for a window
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub floating_window_aspect_ratio: Option<AspectRatio>,
|
||||
/// Which Windows API behaviour to use when manipulating windows (default: Sync)
|
||||
/// Which Windows API behaviour to use when manipulating windows
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = WindowHandlingBehaviour::Sync)))]
|
||||
pub window_handling_behaviour: Option<WindowHandlingBehaviour>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Animations configuration options
|
||||
pub struct AnimationsConfig {
|
||||
/// Enable or disable animations (default: false)
|
||||
/// Enable or disable animations
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = PerAnimationPrefixConfig::Global(false))))]
|
||||
pub enabled: PerAnimationPrefixConfig<bool>,
|
||||
/// Set the animation duration in ms (default: 250)
|
||||
/// Set the animation duration in ms
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = PerAnimationPrefixConfig::Global(250))))]
|
||||
pub duration: Option<PerAnimationPrefixConfig<u64>>,
|
||||
/// Set the animation style (default: Linear)
|
||||
/// Set the animation style
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = PerAnimationPrefixConfig::Global(AnimationStyle::Linear))))]
|
||||
pub style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
|
||||
/// Set the animation FPS (default: 60)
|
||||
/// Set the animation FPS
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = ANIMATION_FPS)))]
|
||||
pub fps: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
pub enum KomorebiTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
Catppuccin {
|
||||
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
|
||||
name: komorebi_themes::Catppuccin,
|
||||
/// Border colour when the container contains a single window (default: Blue)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
single_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the container contains multiple windows (default: Green)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stack_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the container is in monocle mode (default: Pink)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
monocle_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the window is floating (default: Yellow)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
floating_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the container is unfocused (default: Base)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unfocused_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Border colour when the container is unfocused and locked (default: Red)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unfocused_locked_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Stackbar focused tab text colour (default: Green)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_focused_text: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Stackbar unfocused tab text colour (default: Text)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_unfocused_text: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Stackbar tab background colour (default: Base)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_background: Option<komorebi_themes::CatppuccinValue>,
|
||||
/// Komorebi status bar accent (default: Blue)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
bar_accent: Option<komorebi_themes::CatppuccinValue>,
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
Base16 {
|
||||
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
|
||||
name: komorebi_themes::Base16,
|
||||
/// Border colour when the container contains a single window (default: Base0D)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
single_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container contains multiple windows (default: Base0B)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stack_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is in monocle mode (default: Base0F)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
monocle_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the window is floating (default: Base09)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
floating_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is unfocused (default: Base01)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unfocused_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is unfocused and locked (default: Base08)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unfocused_locked_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar focused tab text colour (default: Base0B)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_focused_text: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar unfocused tab text colour (default: Base05)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar tab background colour (default: Base01)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_background: Option<komorebi_themes::Base16Value>,
|
||||
/// Komorebi status bar accent (default: Base0D)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
bar_accent: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
/// A custom Base16 theme
|
||||
Custom {
|
||||
/// Colours of the custom Base16 theme palette
|
||||
colours: Box<komorebi_themes::Base16ColourPalette>,
|
||||
/// Border colour when the container contains a single window (default: Base0D)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
single_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container contains multiple windows (default: Base0B)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stack_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is in monocle mode (default: Base0F)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
monocle_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the window is floating (default: Base09)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
floating_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is unfocused (default: Base01)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unfocused_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Border colour when the container is unfocused and locked (default: Base08)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
unfocused_locked_border: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar focused tab text colour (default: Base0B)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_focused_text: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar unfocused tab text colour (default: Base05)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
|
||||
/// Stackbar tab background colour (default: Base01)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stackbar_background: Option<komorebi_themes::Base16Value>,
|
||||
/// Komorebi status bar accent (default: Base0D)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
bar_accent: Option<komorebi_themes::Base16Value>,
|
||||
},
|
||||
}
|
||||
pub use komorebi_themes::KomorebiTheme;
|
||||
|
||||
impl StaticConfig {
|
||||
pub fn end_of_life(raw: &str) {
|
||||
@@ -796,6 +757,7 @@ impl StaticConfig {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Stackbar tabs configuration
|
||||
pub struct TabsConfig {
|
||||
/// Width of a stackbar tab
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -819,6 +781,7 @@ pub struct TabsConfig {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Stackbar configuration
|
||||
pub struct StackbarConfig {
|
||||
/// Stackbar height
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -826,8 +789,9 @@ pub struct StackbarConfig {
|
||||
/// Stackbar label
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub label: Option<StackbarLabel>,
|
||||
/// Stackbar mode (default: Never)
|
||||
/// Stackbar mode
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = StackbarMode::Never)))]
|
||||
pub mode: Option<StackbarMode>,
|
||||
/// Stackbar tab configuration options
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -862,6 +826,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
};
|
||||
|
||||
Self {
|
||||
#[allow(deprecated)]
|
||||
invisible_borders: None,
|
||||
resize_delta: Option::from(value.resize_delta),
|
||||
window_container_behaviour: Option::from(
|
||||
@@ -890,6 +855,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
),
|
||||
minimum_window_height: Some(window::MINIMUM_HEIGHT.load(Ordering::SeqCst)),
|
||||
minimum_window_width: Some(window::MINIMUM_WIDTH.load(Ordering::SeqCst)),
|
||||
#[allow(deprecated)]
|
||||
focus_follows_mouse: value.focus_follows_mouse,
|
||||
mouse_follows_focus: Option::from(value.mouse_follows_focus),
|
||||
app_specific_configuration_path: None,
|
||||
@@ -905,6 +871,7 @@ impl From<&WindowManager> for StaticConfig {
|
||||
),
|
||||
transparency_ignore_rules: None,
|
||||
border_style: Option::from(STYLE.load()),
|
||||
#[allow(deprecated)]
|
||||
border_z_order: None,
|
||||
border_implementation: Option::from(IMPLEMENTATION.load()),
|
||||
default_workspace_padding: Option::from(
|
||||
@@ -1028,9 +995,17 @@ impl StaticConfig {
|
||||
DEFAULT_WORKSPACE_PADDING.store(workspace, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
border_manager::BORDER_WIDTH.store(self.border_width.unwrap_or(8), Ordering::SeqCst);
|
||||
border_manager::BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
|
||||
border_manager::BORDER_ENABLED.store(self.border.unwrap_or(true), Ordering::SeqCst);
|
||||
if let Some(border_width) = self.border_width {
|
||||
border_manager::BORDER_WIDTH.store(border_width, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(border_offset) = self.border_offset {
|
||||
border_manager::BORDER_OFFSET.store(border_offset, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(border_enabled) = self.border {
|
||||
border_manager::BORDER_ENABLED.store(border_enabled, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(colours) = &self.border_colours {
|
||||
if let Some(single) = colours.single {
|
||||
@@ -1084,10 +1059,14 @@ impl StaticConfig {
|
||||
border_manager::send_notification(None);
|
||||
}
|
||||
|
||||
transparency_manager::TRANSPARENCY_ENABLED
|
||||
.store(self.transparency.unwrap_or(false), Ordering::SeqCst);
|
||||
transparency_manager::TRANSPARENCY_ALPHA
|
||||
.store(self.transparency_alpha.unwrap_or(200), Ordering::SeqCst);
|
||||
if let Some(transparency_enabled) = self.transparency {
|
||||
transparency_manager::TRANSPARENCY_ENABLED
|
||||
.store(transparency_enabled, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Some(transparency_alpha) = self.transparency_alpha {
|
||||
transparency_manager::TRANSPARENCY_ALPHA.store(transparency_alpha, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
|
||||
let mut regex_identifiers = REGEX_IDENTIFIERS.lock();
|
||||
@@ -1311,9 +1290,12 @@ impl StaticConfig {
|
||||
unmanaged_window_operation_behaviour: value
|
||||
.unmanaged_window_operation_behaviour
|
||||
.unwrap_or(OperationBehaviour::Op),
|
||||
resize_delta: value.resize_delta.unwrap_or(50),
|
||||
resize_delta: value.resize_delta.unwrap_or(DEFAULT_RESIZE_DELTA),
|
||||
#[allow(deprecated)]
|
||||
focus_follows_mouse: value.focus_follows_mouse,
|
||||
mouse_follows_focus: value.mouse_follows_focus.unwrap_or(true),
|
||||
mouse_follows_focus: value
|
||||
.mouse_follows_focus
|
||||
.unwrap_or(DEFAULT_MOUSE_FOLLOWS_FOCUS),
|
||||
hotwatch: Hotwatch::new()?,
|
||||
has_pending_raise_op: false,
|
||||
pending_move_op: Arc::new(None),
|
||||
@@ -1322,6 +1304,7 @@ impl StaticConfig {
|
||||
known_hwnds: HashMap::new(),
|
||||
};
|
||||
|
||||
#[allow(deprecated)]
|
||||
match value.focus_follows_mouse {
|
||||
None => WindowsApi::disable_focus_follows_mouse()?,
|
||||
Some(FocusFollowsMouseImplementation::Windows) => {
|
||||
@@ -1679,7 +1662,10 @@ impl StaticConfig {
|
||||
|
||||
wm.enforce_workspace_rules()?;
|
||||
|
||||
border_manager::BORDER_ENABLED.store(value.border.unwrap_or(true), Ordering::SeqCst);
|
||||
if let Some(border_enabled) = value.border {
|
||||
border_manager::BORDER_ENABLED.store(border_enabled, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
wm.window_management_behaviour.current_behaviour =
|
||||
value.window_container_behaviour.unwrap_or_default();
|
||||
wm.window_management_behaviour.float_override = value.float_override.unwrap_or_default();
|
||||
@@ -1699,10 +1685,15 @@ impl StaticConfig {
|
||||
wm.unmanaged_window_operation_behaviour = value
|
||||
.unmanaged_window_operation_behaviour
|
||||
.unwrap_or_default();
|
||||
wm.resize_delta = value.resize_delta.unwrap_or(50);
|
||||
wm.mouse_follows_focus = value.mouse_follows_focus.unwrap_or(true);
|
||||
wm.resize_delta = value.resize_delta.unwrap_or(DEFAULT_RESIZE_DELTA);
|
||||
wm.mouse_follows_focus = value
|
||||
.mouse_follows_focus
|
||||
.unwrap_or(DEFAULT_MOUSE_FOLLOWS_FOCUS);
|
||||
wm.work_area_offset = value.global_work_area_offset;
|
||||
wm.focus_follows_mouse = value.focus_follows_mouse;
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
wm.focus_follows_mouse = value.focus_follows_mouse;
|
||||
}
|
||||
|
||||
match wm.focus_follows_mouse {
|
||||
None => WindowsApi::disable_focus_follows_mouse()?,
|
||||
@@ -1928,7 +1919,7 @@ mod tests {
|
||||
let docs = vec![
|
||||
"0.1.20", "0.1.21", "0.1.22", "0.1.23", "0.1.24", "0.1.25", "0.1.26", "0.1.27",
|
||||
"0.1.28", "0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33", "0.1.34", "0.1.35",
|
||||
"0.1.36", "0.1.37",
|
||||
"0.1.36", "0.1.37", "0.1.38", "0.1.39",
|
||||
];
|
||||
|
||||
let mut versions = vec![];
|
||||
@@ -1969,6 +1960,7 @@ mod tests {
|
||||
"#;
|
||||
let config = serde_json::from_str::<WorkspaceConfig>(config).unwrap();
|
||||
|
||||
#[allow(deprecated)]
|
||||
let custom_layout_rules = config.custom_layout_rules.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
@@ -1985,7 +1977,10 @@ mod tests {
|
||||
"name": "Test",
|
||||
}
|
||||
"#;
|
||||
|
||||
let config = serde_json::from_str::<WorkspaceConfig>(config).unwrap();
|
||||
assert_eq!(config.custom_layout_rules, None);
|
||||
#[allow(deprecated)]
|
||||
let custom_layout_rules = config.custom_layout_rules;
|
||||
assert_eq!(custom_layout_rules, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossbeam_utils::atomic::AtomicCell;
|
||||
use komorebi_themes::Base16Wrapper;
|
||||
use komorebi_themes::KomorebiThemeBase16 as Base16;
|
||||
use komorebi_themes::KomorebiThemeCatppuccin as Catppuccin;
|
||||
use komorebi_themes::KomorebiThemeCustom as Custom;
|
||||
use komorebi_themes::colour::Colour;
|
||||
use std::ops::Deref;
|
||||
use std::sync::OnceLock;
|
||||
@@ -84,7 +87,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
) = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
KomorebiTheme::Catppuccin(Catppuccin {
|
||||
name,
|
||||
single_border,
|
||||
stack_border,
|
||||
@@ -96,7 +99,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
}) => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::CatppuccinValue::Blue)
|
||||
.color32(name.as_theme());
|
||||
@@ -145,7 +148,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
KomorebiTheme::Base16 {
|
||||
KomorebiTheme::Base16(Base16 {
|
||||
name,
|
||||
single_border,
|
||||
stack_border,
|
||||
@@ -157,7 +160,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
}) => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0D)
|
||||
.color32(Base16Wrapper::Base16(*name));
|
||||
@@ -206,7 +209,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
stackbar_background,
|
||||
)
|
||||
}
|
||||
KomorebiTheme::Custom {
|
||||
KomorebiTheme::Custom(Custom {
|
||||
colours,
|
||||
single_border,
|
||||
stack_border,
|
||||
@@ -218,7 +221,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
|
||||
stackbar_unfocused_text,
|
||||
stackbar_background,
|
||||
..
|
||||
} => {
|
||||
}) => {
|
||||
let single_border = single_border
|
||||
.unwrap_or(komorebi_themes::Base16Value::Base0D)
|
||||
.color32(Base16Wrapper::Custom(colours.clone()));
|
||||
|
||||
@@ -302,10 +302,13 @@ impl RenderDispatcher for TransparencyRenderDispatcher {
|
||||
#[derive(Copy, Clone, Debug, Display, EnumString, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
/// Aspect ratio for temporarily floating windows
|
||||
pub enum AspectRatio {
|
||||
/// A predefined aspect ratio
|
||||
/// Predefined aspect ratio
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Predefined"))]
|
||||
Predefined(PredefinedAspectRatio),
|
||||
/// A custom W:H aspect ratio
|
||||
/// Custom W:H aspect ratio
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
Custom(i32, i32),
|
||||
}
|
||||
|
||||
@@ -317,6 +320,7 @@ impl Default for AspectRatio {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Display, EnumString, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// Predefined aspect ratio
|
||||
pub enum PredefinedAspectRatio {
|
||||
/// 21:9
|
||||
Ultrawide,
|
||||
@@ -491,6 +495,8 @@ impl Window {
|
||||
}
|
||||
|
||||
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
|
||||
|
||||
#[allow(deprecated)]
|
||||
match *hiding_behaviour {
|
||||
HidingBehaviour::Hide => WindowsApi::hide_window(self.hwnd),
|
||||
HidingBehaviour::Minimize => WindowsApi::minimize_window(self.hwnd),
|
||||
@@ -515,6 +521,8 @@ impl Window {
|
||||
}
|
||||
|
||||
let hiding_behaviour = HIDING_BEHAVIOUR.lock();
|
||||
|
||||
#[allow(deprecated)]
|
||||
match *hiding_behaviour {
|
||||
HidingBehaviour::Hide | HidingBehaviour::Minimize => {
|
||||
WindowsApi::restore_window(self.hwnd);
|
||||
|
||||
@@ -19,8 +19,6 @@ use hotwatch::EventKind;
|
||||
use hotwatch::Hotwatch;
|
||||
use hotwatch::notify::ErrorKind as NotifyErrorKind;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixListener;
|
||||
use uds_windows::UnixStream;
|
||||
|
||||
@@ -30,69 +28,40 @@ use crate::animation::AnimationEngine;
|
||||
use crate::core::Arrangement;
|
||||
use crate::core::Axis;
|
||||
use crate::core::BorderImplementation;
|
||||
use crate::core::BorderStyle;
|
||||
use crate::core::CustomLayout;
|
||||
use crate::core::CycleDirection;
|
||||
use crate::core::DefaultLayout;
|
||||
use crate::core::FocusFollowsMouseImplementation;
|
||||
use crate::core::HidingBehaviour;
|
||||
use crate::core::Layout;
|
||||
use crate::core::MoveBehaviour;
|
||||
use crate::core::OperationBehaviour;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
use crate::core::Sizing;
|
||||
use crate::core::StackbarLabel;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowManagementBehaviour;
|
||||
use crate::core::config_generation::MatchingRule;
|
||||
use crate::core::custom_layout::CustomLayout;
|
||||
|
||||
use crate::BorderColours;
|
||||
use crate::CUSTOM_FFM;
|
||||
use crate::Colour;
|
||||
use crate::CrossBoundaryBehaviour;
|
||||
use crate::DATA_DIR;
|
||||
use crate::DISPLAY_INDEX_PREFERENCES;
|
||||
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
|
||||
use crate::HIDING_BEHAVIOUR;
|
||||
use crate::HOME_DIR;
|
||||
use crate::IGNORE_IDENTIFIERS;
|
||||
use crate::LAYERED_WHITELIST;
|
||||
use crate::MANAGE_IDENTIFIERS;
|
||||
use crate::MONITOR_INDEX_PREFERENCES;
|
||||
use crate::NO_TITLEBAR;
|
||||
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
|
||||
use crate::REGEX_IDENTIFIERS;
|
||||
use crate::REMOVE_TITLEBARS;
|
||||
use crate::Rgb;
|
||||
use crate::SUBSCRIPTION_SOCKETS;
|
||||
use crate::TRANSPARENCY_BLACKLIST;
|
||||
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
|
||||
use crate::WORKSPACE_MATCHING_RULES;
|
||||
use crate::border_manager;
|
||||
use crate::border_manager::BORDER_OFFSET;
|
||||
use crate::border_manager::BORDER_WIDTH;
|
||||
use crate::border_manager::STYLE;
|
||||
use crate::config_generation::WorkspaceMatchingRule;
|
||||
use crate::container::Container;
|
||||
use crate::core::StackbarMode;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::load_configuration;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::ring::Ring;
|
||||
use crate::should_act;
|
||||
use crate::should_act_individual;
|
||||
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_LABEL;
|
||||
use crate::stackbar_manager::STACKBAR_MODE;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_HEIGHT;
|
||||
use crate::stackbar_manager::STACKBAR_TAB_WIDTH;
|
||||
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
|
||||
use crate::state::State;
|
||||
use crate::static_config::StaticConfig;
|
||||
use crate::transparency_manager;
|
||||
use crate::transparency_manager::TRANSPARENCY_ALPHA;
|
||||
use crate::transparency_manager::TRANSPARENCY_ENABLED;
|
||||
use crate::window::Window;
|
||||
use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
@@ -125,263 +94,12 @@ pub struct WindowManager {
|
||||
pub known_hwnds: HashMap<isize, (usize, usize)>,
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct State {
|
||||
pub monitors: Ring<Monitor>,
|
||||
pub monitor_usr_idx_map: HashMap<usize, usize>,
|
||||
pub is_paused: bool,
|
||||
pub resize_delta: i32,
|
||||
pub new_window_behaviour: WindowContainerBehaviour,
|
||||
pub float_override: bool,
|
||||
pub cross_monitor_move_behaviour: MoveBehaviour,
|
||||
pub unmanaged_window_operation_behaviour: OperationBehaviour,
|
||||
pub work_area_offset: Option<Rect>,
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouseImplementation>,
|
||||
pub mouse_follows_focus: bool,
|
||||
pub has_pending_raise_op: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn has_been_modified(&self, wm: &WindowManager) -> bool {
|
||||
let new = Self::from(wm);
|
||||
|
||||
if self.monitors != new.monitors {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.is_paused != new.is_paused {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.new_window_behaviour != new.new_window_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.float_override != new.float_override {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.cross_monitor_move_behaviour != new.cross_monitor_move_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.unmanaged_window_operation_behaviour != new.unmanaged_window_operation_behaviour {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.work_area_offset != new.work_area_offset {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.focus_follows_mouse != new.focus_follows_mouse {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.mouse_follows_focus != new.mouse_follows_focus {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.has_pending_raise_op != new.has_pending_raise_op {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct GlobalState {
|
||||
pub border_enabled: bool,
|
||||
pub border_colours: BorderColours,
|
||||
pub border_style: BorderStyle,
|
||||
pub border_offset: i32,
|
||||
pub border_width: i32,
|
||||
pub stackbar_mode: StackbarMode,
|
||||
pub stackbar_label: StackbarLabel,
|
||||
pub stackbar_focused_text_colour: Colour,
|
||||
pub stackbar_unfocused_text_colour: Colour,
|
||||
pub stackbar_tab_background_colour: Colour,
|
||||
pub stackbar_tab_width: i32,
|
||||
pub stackbar_height: i32,
|
||||
pub transparency_enabled: bool,
|
||||
pub transparency_alpha: u8,
|
||||
pub transparency_blacklist: Vec<MatchingRule>,
|
||||
pub remove_titlebars: bool,
|
||||
#[serde(alias = "float_identifiers")]
|
||||
pub ignore_identifiers: Vec<MatchingRule>,
|
||||
pub manage_identifiers: Vec<MatchingRule>,
|
||||
pub layered_whitelist: Vec<MatchingRule>,
|
||||
pub tray_and_multi_window_identifiers: Vec<MatchingRule>,
|
||||
pub name_change_on_launch_identifiers: Vec<MatchingRule>,
|
||||
pub monitor_index_preferences: HashMap<usize, Rect>,
|
||||
pub display_index_preferences: HashMap<usize, String>,
|
||||
pub ignored_duplicate_monitor_serial_ids: Vec<String>,
|
||||
pub workspace_rules: Vec<WorkspaceMatchingRule>,
|
||||
pub window_hiding_behaviour: HidingBehaviour,
|
||||
pub configuration_dir: PathBuf,
|
||||
pub data_dir: PathBuf,
|
||||
pub custom_ffm: bool,
|
||||
}
|
||||
|
||||
impl Default for GlobalState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
border_enabled: border_manager::BORDER_ENABLED.load(Ordering::SeqCst),
|
||||
border_colours: BorderColours {
|
||||
single: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::FOCUSED.load(Ordering::SeqCst),
|
||||
))),
|
||||
stack: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::STACK.load(Ordering::SeqCst),
|
||||
))),
|
||||
monocle: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::MONOCLE.load(Ordering::SeqCst),
|
||||
))),
|
||||
floating: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::FLOATING.load(Ordering::SeqCst),
|
||||
))),
|
||||
unfocused: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::UNFOCUSED.load(Ordering::SeqCst),
|
||||
))),
|
||||
unfocused_locked: Option::from(Colour::Rgb(Rgb::from(
|
||||
border_manager::UNFOCUSED_LOCKED.load(Ordering::SeqCst),
|
||||
))),
|
||||
},
|
||||
border_style: STYLE.load(),
|
||||
border_offset: border_manager::BORDER_OFFSET.load(Ordering::SeqCst),
|
||||
border_width: border_manager::BORDER_WIDTH.load(Ordering::SeqCst),
|
||||
stackbar_mode: STACKBAR_MODE.load(),
|
||||
stackbar_label: STACKBAR_LABEL.load(),
|
||||
stackbar_focused_text_colour: Colour::Rgb(Rgb::from(
|
||||
STACKBAR_FOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
|
||||
)),
|
||||
stackbar_unfocused_text_colour: Colour::Rgb(Rgb::from(
|
||||
STACKBAR_UNFOCUSED_TEXT_COLOUR.load(Ordering::SeqCst),
|
||||
)),
|
||||
stackbar_tab_background_colour: Colour::Rgb(Rgb::from(
|
||||
STACKBAR_TAB_BACKGROUND_COLOUR.load(Ordering::SeqCst),
|
||||
)),
|
||||
stackbar_tab_width: STACKBAR_TAB_WIDTH.load(Ordering::SeqCst),
|
||||
stackbar_height: STACKBAR_TAB_HEIGHT.load(Ordering::SeqCst),
|
||||
transparency_enabled: TRANSPARENCY_ENABLED.load(Ordering::SeqCst),
|
||||
transparency_alpha: TRANSPARENCY_ALPHA.load(Ordering::SeqCst),
|
||||
transparency_blacklist: TRANSPARENCY_BLACKLIST.lock().clone(),
|
||||
remove_titlebars: REMOVE_TITLEBARS.load(Ordering::SeqCst),
|
||||
ignore_identifiers: IGNORE_IDENTIFIERS.lock().clone(),
|
||||
manage_identifiers: MANAGE_IDENTIFIERS.lock().clone(),
|
||||
layered_whitelist: LAYERED_WHITELIST.lock().clone(),
|
||||
tray_and_multi_window_identifiers: TRAY_AND_MULTI_WINDOW_IDENTIFIERS.lock().clone(),
|
||||
name_change_on_launch_identifiers: OBJECT_NAME_CHANGE_ON_LAUNCH.lock().clone(),
|
||||
monitor_index_preferences: MONITOR_INDEX_PREFERENCES.lock().clone(),
|
||||
display_index_preferences: DISPLAY_INDEX_PREFERENCES.read().clone(),
|
||||
ignored_duplicate_monitor_serial_ids: DUPLICATE_MONITOR_SERIAL_IDS.read().clone(),
|
||||
workspace_rules: WORKSPACE_MATCHING_RULES.lock().clone(),
|
||||
window_hiding_behaviour: *HIDING_BEHAVIOUR.lock(),
|
||||
configuration_dir: HOME_DIR.clone(),
|
||||
data_dir: DATA_DIR.clone(),
|
||||
custom_ffm: CUSTOM_FFM.load(Ordering::SeqCst),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Self> for WindowManager {
|
||||
fn as_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&WindowManager> for State {
|
||||
fn from(wm: &WindowManager) -> Self {
|
||||
// This is used to remove any information that doesn't need to be passed on to subscribers
|
||||
// or to be shown with the `komorebic state` command. Currently it is only removing the
|
||||
// `workspace_config` field from every workspace, but more stripping can be added later if
|
||||
// needed.
|
||||
let mut stripped_monitors = Ring::default();
|
||||
*stripped_monitors.elements_mut() = wm
|
||||
.monitors()
|
||||
.iter()
|
||||
.map(|monitor| Monitor {
|
||||
id: monitor.id,
|
||||
name: monitor.name.clone(),
|
||||
device: monitor.device.clone(),
|
||||
device_id: monitor.device_id.clone(),
|
||||
serial_number_id: monitor.serial_number_id.clone(),
|
||||
size: monitor.size,
|
||||
work_area_size: monitor.work_area_size,
|
||||
work_area_offset: monitor.work_area_offset,
|
||||
window_based_work_area_offset: monitor.window_based_work_area_offset,
|
||||
window_based_work_area_offset_limit: monitor.window_based_work_area_offset_limit,
|
||||
workspaces: {
|
||||
let mut ws = Ring::default();
|
||||
*ws.elements_mut() = monitor
|
||||
.workspaces()
|
||||
.iter()
|
||||
.map(|workspace| Workspace {
|
||||
name: workspace.name.clone(),
|
||||
containers: workspace.containers.clone(),
|
||||
monocle_container: workspace.monocle_container.clone(),
|
||||
monocle_container_restore_idx: workspace.monocle_container_restore_idx,
|
||||
maximized_window: workspace.maximized_window,
|
||||
maximized_window_restore_idx: workspace.maximized_window_restore_idx,
|
||||
floating_windows: workspace.floating_windows.clone(),
|
||||
layout: workspace.layout.clone(),
|
||||
layout_options: workspace.layout_options,
|
||||
layout_rules: workspace.layout_rules.clone(),
|
||||
layout_flip: workspace.layout_flip,
|
||||
workspace_padding: workspace.workspace_padding,
|
||||
container_padding: workspace.container_padding,
|
||||
latest_layout: workspace.latest_layout.clone(),
|
||||
resize_dimensions: workspace.resize_dimensions.clone(),
|
||||
tile: workspace.tile,
|
||||
work_area_offset: workspace.work_area_offset,
|
||||
apply_window_based_work_area_offset: workspace
|
||||
.apply_window_based_work_area_offset,
|
||||
window_container_behaviour: workspace.window_container_behaviour,
|
||||
window_container_behaviour_rules: workspace
|
||||
.window_container_behaviour_rules
|
||||
.clone(),
|
||||
float_override: workspace.float_override,
|
||||
layer: workspace.layer,
|
||||
floating_layer_behaviour: workspace.floating_layer_behaviour,
|
||||
globals: workspace.globals,
|
||||
wallpaper: workspace.wallpaper.clone(),
|
||||
workspace_config: None,
|
||||
})
|
||||
.collect::<VecDeque<_>>();
|
||||
ws.focus(monitor.workspaces.focused_idx());
|
||||
ws
|
||||
},
|
||||
last_focused_workspace: monitor.last_focused_workspace,
|
||||
workspace_names: monitor.workspace_names.clone(),
|
||||
container_padding: monitor.container_padding,
|
||||
workspace_padding: monitor.workspace_padding,
|
||||
wallpaper: monitor.wallpaper.clone(),
|
||||
floating_layer_behaviour: monitor.floating_layer_behaviour,
|
||||
})
|
||||
.collect::<VecDeque<_>>();
|
||||
stripped_monitors.focus(wm.monitors.focused_idx());
|
||||
|
||||
Self {
|
||||
monitors: stripped_monitors,
|
||||
monitor_usr_idx_map: wm.monitor_usr_idx_map.clone(),
|
||||
is_paused: wm.is_paused,
|
||||
work_area_offset: wm.work_area_offset,
|
||||
resize_delta: wm.resize_delta,
|
||||
new_window_behaviour: wm.window_management_behaviour.current_behaviour,
|
||||
float_override: wm.window_management_behaviour.float_override,
|
||||
cross_monitor_move_behaviour: wm.cross_monitor_move_behaviour,
|
||||
focus_follows_mouse: wm.focus_follows_mouse,
|
||||
mouse_follows_focus: wm.mouse_follows_focus,
|
||||
has_pending_raise_op: wm.has_pending_raise_op,
|
||||
unmanaged_window_operation_behaviour: wm.unmanaged_window_operation_behaviour,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_ring_elements!(WindowManager, Monitor);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -525,14 +243,16 @@ impl WindowManager {
|
||||
if let Some(state_monitor) = state.monitors.elements().get(monitor_idx)
|
||||
&& let Some(state_workspace) = state_monitor.workspaces().get(workspace_idx)
|
||||
{
|
||||
// to make sure padding changes get applied for users after a quick restart
|
||||
// to make sure padding and layout_options changes get applied for users after a quick restart
|
||||
let container_padding = workspace.container_padding;
|
||||
let workspace_padding = workspace.workspace_padding;
|
||||
let layout_options = workspace.layout_options;
|
||||
|
||||
*workspace = state_workspace.clone();
|
||||
|
||||
workspace.container_padding = container_padding;
|
||||
workspace.workspace_padding = workspace_padding;
|
||||
workspace.layout_options = layout_options;
|
||||
|
||||
if state_monitor.focused_workspace_idx() == workspace_idx {
|
||||
focused_workspace = workspace_idx;
|
||||
@@ -1557,6 +1277,7 @@ impl WindowManager {
|
||||
workspace.layout_flip,
|
||||
focused_idx,
|
||||
len,
|
||||
workspace.layout_options,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
@@ -2323,6 +2044,53 @@ impl WindowManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn preselect_container_in_direction(
|
||||
&mut self,
|
||||
direction: OperationDirection,
|
||||
) -> eyre::Result<()> {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let focused_idx = workspace.focused_container_idx();
|
||||
|
||||
if matches!(workspace.layout, Layout::Default(DefaultLayout::Grid)) {
|
||||
tracing::warn!("preselection is not supported on the grid layout");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!("preselecting container");
|
||||
|
||||
let new_idx =
|
||||
if workspace.maximized_window.is_some() || workspace.monocle_container.is_some() {
|
||||
None
|
||||
} else {
|
||||
workspace.new_idx_for_direction(direction)
|
||||
};
|
||||
|
||||
match new_idx {
|
||||
Some(new_idx) => {
|
||||
let adjusted_idx = match direction {
|
||||
OperationDirection::Left | OperationDirection::Up => {
|
||||
if focused_idx.abs_diff(new_idx) == 1 {
|
||||
new_idx + 1
|
||||
} else {
|
||||
new_idx
|
||||
}
|
||||
}
|
||||
_ => new_idx,
|
||||
};
|
||||
|
||||
workspace.preselect_container_idx(adjusted_idx);
|
||||
}
|
||||
None => {
|
||||
tracing::debug!(
|
||||
"this is not a valid preselection direction from the current position"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn focus_container_in_direction(
|
||||
&mut self,
|
||||
@@ -2963,8 +2731,6 @@ impl WindowManager {
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn stack_all(&mut self) -> eyre::Result<()> {
|
||||
self.unstack_all(false)?;
|
||||
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
tracing::info!("stacking all windows on workspace");
|
||||
|
||||
@@ -2977,16 +2743,27 @@ impl WindowManager {
|
||||
focused_hwnd = Some(window.hwnd);
|
||||
}
|
||||
|
||||
workspace.focus_container(workspace.containers().len().saturating_sub(1));
|
||||
while workspace.focused_container_idx() > 0 {
|
||||
workspace.move_window_to_container(0)?;
|
||||
workspace.focus_container(workspace.containers().len().saturating_sub(1));
|
||||
}
|
||||
let workspace_hwnds =
|
||||
workspace
|
||||
.containers()
|
||||
.iter()
|
||||
.fold(VecDeque::new(), |mut hwnds, c| {
|
||||
hwnds.extend(c.windows().clone());
|
||||
hwnds
|
||||
});
|
||||
|
||||
if let Some(hwnd) = focused_hwnd {
|
||||
workspace.focus_container_by_window(hwnd)?;
|
||||
}
|
||||
let mut container = Container::default();
|
||||
*container.windows_mut() = workspace_hwnds;
|
||||
*workspace.containers_mut() = VecDeque::from([container]);
|
||||
workspace.focus_container(0);
|
||||
|
||||
if let Some(hwnd) = focused_hwnd
|
||||
&& let Some(c) = workspace.focused_container_mut()
|
||||
&& let Some(w_idx_to_focus) = c.idx_for_window(hwnd)
|
||||
{
|
||||
c.focus_window(w_idx_to_focus);
|
||||
c.load_focused_window();
|
||||
}
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
}
|
||||
|
||||
@@ -3045,6 +2822,7 @@ impl WindowManager {
|
||||
workspace.layout_flip,
|
||||
workspace.focused_container_idx(),
|
||||
len,
|
||||
workspace.layout_options,
|
||||
)
|
||||
.is_some();
|
||||
|
||||
@@ -3116,6 +2894,39 @@ impl WindowManager {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn promote_container_swap(&mut self) -> eyre::Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let focused_container_idx = workspace.focused_container_idx();
|
||||
|
||||
let primary_idx = match &workspace.layout {
|
||||
Layout::Default(_) => 0,
|
||||
Layout::Custom(layout) => layout.first_container_idx(
|
||||
layout
|
||||
.primary_idx()
|
||||
.ok_or_eyre("this custom layout does not have a primary column")?,
|
||||
),
|
||||
};
|
||||
|
||||
if matches!(workspace.layout, Layout::Default(DefaultLayout::Grid)) {
|
||||
tracing::debug!("ignoring promote-swap command for grid layout");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let primary_tile_is_focused = focused_container_idx == primary_idx;
|
||||
|
||||
if primary_tile_is_focused && let Some(swap_idx) = workspace.promotion_swap_container_idx {
|
||||
workspace.swap_containers(focused_container_idx, swap_idx);
|
||||
} else {
|
||||
workspace.promotion_swap_container_idx = Some(focused_container_idx);
|
||||
workspace.swap_containers(focused_container_idx, primary_idx);
|
||||
}
|
||||
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn promote_focus_to_front(&mut self) -> eyre::Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::mem::size_of;
|
||||
use std::path::Path;
|
||||
use windows::Win32::Foundation::COLORREF;
|
||||
use windows::Win32::Foundation::CloseHandle;
|
||||
use windows::Win32::Foundation::GetLastError;
|
||||
use windows::Win32::Foundation::HANDLE;
|
||||
use windows::Win32::Foundation::HINSTANCE;
|
||||
use windows::Win32::Foundation::HMODULE;
|
||||
@@ -17,6 +18,8 @@ use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::Foundation::LPARAM;
|
||||
use windows::Win32::Foundation::POINT;
|
||||
use windows::Win32::Foundation::RECT;
|
||||
use windows::Win32::Foundation::SetLastError;
|
||||
use windows::Win32::Foundation::WIN32_ERROR;
|
||||
use windows::Win32::Foundation::WPARAM;
|
||||
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_APP;
|
||||
use windows::Win32::Graphics::Dwm::DWM_CLOAKED_INHERITED;
|
||||
@@ -911,18 +914,42 @@ impl WindowsApi {
|
||||
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> eyre::Result<isize> {
|
||||
// Can return 0, which does not always mean that an error has occurred
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindowLongPtrW(hwnd, index)
|
||||
}))
|
||||
unsafe {
|
||||
SetLastError(WIN32_ERROR(0));
|
||||
let result = GetWindowLongPtrW(hwnd, index);
|
||||
|
||||
if result != 0 {
|
||||
Ok(result)
|
||||
} else {
|
||||
let last_error = GetLastError();
|
||||
if last_error == WIN32_ERROR(0) {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(std::io::Error::from_raw_os_error(last_error.0 as i32).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
fn window_long_ptr_w(hwnd: HWND, index: WINDOW_LONG_PTR_INDEX) -> eyre::Result<i32> {
|
||||
// Can return 0, which does not always mean that an error has occurred
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw
|
||||
Result::from(WindowsResult::from(unsafe {
|
||||
GetWindowLongPtrW(hwnd, index)
|
||||
}))
|
||||
unsafe {
|
||||
SetLastError(WIN32_ERROR(0));
|
||||
let result = GetWindowLongPtrW(hwnd, index);
|
||||
|
||||
if result != 0 {
|
||||
Ok(result)
|
||||
} else {
|
||||
let last_error = GetLastError();
|
||||
if last_error == WIN32_ERROR(0) {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(std::io::Error::from_raw_os_error(last_error.0 as i32).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
|
||||
@@ -25,9 +25,9 @@ use crate::core::CustomLayout;
|
||||
use crate::core::CycleDirection;
|
||||
use crate::core::DefaultLayout;
|
||||
use crate::core::Layout;
|
||||
use crate::core::LayoutOptions;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use crate::lockable_sequence::LockableSequence;
|
||||
use crate::ring::Ring;
|
||||
use crate::should_act;
|
||||
@@ -40,6 +40,7 @@ use crate::windows_api::WindowsApi;
|
||||
use color_eyre::eyre;
|
||||
use color_eyre::eyre::OptionExt;
|
||||
use komorebi_themes::Base16ColourPalette;
|
||||
use komorebi_themes::KomorebiThemeCustom as Custom;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use uds_windows::UnixStream;
|
||||
@@ -78,6 +79,10 @@ pub struct Workspace {
|
||||
pub wallpaper: Option<Wallpaper>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub workspace_config: Option<WorkspaceConfig>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preselected_container_idx: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub promotion_swap_container_idx: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -129,6 +134,8 @@ impl Default for Workspace {
|
||||
globals: Default::default(),
|
||||
workspace_config: None,
|
||||
wallpaper: None,
|
||||
preselected_container_idx: None,
|
||||
promotion_swap_container_idx: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,14 +175,19 @@ impl Workspace {
|
||||
self.layout = Layout::Default(*layout);
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
if let Some(pathbuf) = &config.custom_layout {
|
||||
let layout = CustomLayout::from_path(pathbuf)?;
|
||||
self.layout = Layout::Custom(layout);
|
||||
}
|
||||
|
||||
self.tile =
|
||||
!(config.custom_layout.is_none() && config.layout.is_none() && config.tile.is_none()
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
self.tile = !(config.custom_layout.is_none()
|
||||
&& config.layout.is_none()
|
||||
&& config.tile.is_none()
|
||||
|| config.tile.is_some_and(|tile| !tile));
|
||||
}
|
||||
|
||||
let mut all_layout_rules = vec![];
|
||||
if let Some(layout_rules) = &config.layout_rules {
|
||||
@@ -189,6 +201,7 @@ impl Workspace {
|
||||
|
||||
self.layout_rules = all_layout_rules.clone();
|
||||
|
||||
#[allow(deprecated)]
|
||||
if let Some(layout_rules) = &config.custom_layout_rules {
|
||||
for (count, pathbuf) in layout_rules {
|
||||
let rule = CustomLayout::from_path(pathbuf)?;
|
||||
@@ -229,6 +242,12 @@ impl Workspace {
|
||||
self.wallpaper = config.wallpaper.clone();
|
||||
self.layout_options = config.layout_options;
|
||||
|
||||
tracing::debug!(
|
||||
"Workspace '{}' loaded layout_options: {:?}",
|
||||
self.name.as_deref().unwrap_or("unnamed"),
|
||||
self.layout_options
|
||||
);
|
||||
|
||||
self.workspace_config = Some(config.clone());
|
||||
|
||||
Ok(())
|
||||
@@ -322,7 +341,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
if let Some(palette) = base16_palette {
|
||||
let komorebi_theme = KomorebiTheme::Custom {
|
||||
let komorebi_theme = KomorebiTheme::Custom(Custom {
|
||||
colours: Box::new(palette),
|
||||
single_border: wallpaper
|
||||
.theme_options
|
||||
@@ -361,7 +380,7 @@ impl Workspace {
|
||||
.as_ref()
|
||||
.and_then(|o| o.stackbar_background),
|
||||
bar_accent: wallpaper.theme_options.as_ref().and_then(|o| o.bar_accent),
|
||||
};
|
||||
});
|
||||
|
||||
let bytes = SocketMessage::Theme(Box::new(komorebi_theme)).as_bytes()?;
|
||||
|
||||
@@ -446,7 +465,8 @@ impl Workspace {
|
||||
}
|
||||
|
||||
// make sure we are never holding on to empty containers
|
||||
self.containers_mut().retain(|c| !c.windows().is_empty());
|
||||
self.containers_mut()
|
||||
.retain(|c| c.is_preselect() || !c.windows().is_empty());
|
||||
|
||||
let container_padding = self
|
||||
.container_padding
|
||||
@@ -536,6 +556,11 @@ impl Workspace {
|
||||
} else if let Some(window) = &mut self.maximized_window {
|
||||
window.maximize();
|
||||
} else if !self.containers().is_empty() {
|
||||
tracing::debug!(
|
||||
"Workspace '{}' update() - self.layout_options before calculate: {:?}",
|
||||
self.name.as_deref().unwrap_or("unnamed"),
|
||||
self.layout_options
|
||||
);
|
||||
let mut layouts = self.layout.as_boxed_arrangement().calculate(
|
||||
&adjusted_work_area,
|
||||
NonZeroUsize::new(self.containers().len()).ok_or_eyre(
|
||||
@@ -818,9 +843,10 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn promote_container(&mut self) -> eyre::Result<()> {
|
||||
let resize = self.resize_dimensions.remove(0);
|
||||
let focused_idx = self.focused_container_idx();
|
||||
let container = self
|
||||
.remove_focused_container()
|
||||
.containers_mut()
|
||||
.remove_respecting_locks(focused_idx)
|
||||
.ok_or_eyre("there is no container")?;
|
||||
|
||||
let primary_idx = match &self.layout {
|
||||
@@ -832,9 +858,10 @@ impl Workspace {
|
||||
),
|
||||
};
|
||||
|
||||
let insertion_idx = self.insert_container_at_idx(primary_idx, container);
|
||||
self.resize_dimensions[insertion_idx] = resize;
|
||||
self.focus_container(primary_idx);
|
||||
let insertion_idx = self
|
||||
.containers_mut()
|
||||
.insert_respecting_locks(primary_idx, container);
|
||||
self.focus_container(insertion_idx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -976,6 +1003,18 @@ impl Workspace {
|
||||
container
|
||||
}
|
||||
|
||||
pub fn preselect_container_idx(&mut self, insertion_idx: usize) {
|
||||
self.preselected_container_idx = Some(insertion_idx);
|
||||
self.insert_container_at_idx(insertion_idx, Container::preselect());
|
||||
}
|
||||
|
||||
pub fn cancel_preselect(&mut self) {
|
||||
if let Some(idx) = self.preselected_container_idx {
|
||||
self.containers_mut().remove_respecting_locks(idx);
|
||||
self.preselected_container_idx = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_idx_for_direction(&self, direction: OperationDirection) -> Option<usize> {
|
||||
let len = NonZeroUsize::new(self.containers().len())?;
|
||||
|
||||
@@ -984,6 +1023,7 @@ impl Workspace {
|
||||
self.layout_flip,
|
||||
self.focused_container_idx(),
|
||||
len,
|
||||
self.layout_options,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1074,7 +1114,12 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn new_container_for_window(&mut self, window: Window) {
|
||||
let next_idx = if self.containers().is_empty() {
|
||||
let next_idx = if let Some(idx) = self.preselected_container_idx {
|
||||
let next = idx;
|
||||
self.preselected_container_idx = None;
|
||||
self.remove_container_by_idx(next);
|
||||
next
|
||||
} else if self.containers().is_empty() {
|
||||
0
|
||||
} else {
|
||||
self.focused_container_idx() + 1
|
||||
@@ -1237,7 +1282,7 @@ impl Workspace {
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
// Zero is actually on the left
|
||||
if let Some(mut left) = resize_dimensions[0] {
|
||||
if let Some(left) = resize_dimensions[0].as_mut() {
|
||||
left.top = 0;
|
||||
left.bottom = 0;
|
||||
left.left = 0;
|
||||
@@ -1270,7 +1315,7 @@ impl Workspace {
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
// Zero is actually on the right
|
||||
if let Some(mut left) = resize_dimensions[1] {
|
||||
if let Some(left) = resize_dimensions[1].as_mut() {
|
||||
left.top = 0;
|
||||
left.bottom = 0;
|
||||
left.right = 0;
|
||||
@@ -1301,7 +1346,7 @@ impl Workspace {
|
||||
match resize_dimensions.len() {
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
if let Some(mut left) = resize_dimensions[0] {
|
||||
if let Some(left) = resize_dimensions[0].as_mut() {
|
||||
left.top = 0;
|
||||
left.left = 0;
|
||||
left.right = 0;
|
||||
@@ -1332,14 +1377,14 @@ impl Workspace {
|
||||
// Two windows can only be resized in the middle
|
||||
2 => {
|
||||
// Zero is actually on the right
|
||||
if let Some(mut right) = resize_dimensions[0] {
|
||||
if let Some(right) = resize_dimensions[0].as_mut() {
|
||||
right.top = 0;
|
||||
right.bottom = 0;
|
||||
right.right = 0;
|
||||
}
|
||||
|
||||
// One is on the left
|
||||
if let Some(mut left) = resize_dimensions[1] {
|
||||
if let Some(left) = resize_dimensions[1].as_mut() {
|
||||
left.top = 0;
|
||||
left.bottom = 0;
|
||||
left.left = 0;
|
||||
@@ -1349,13 +1394,13 @@ impl Workspace {
|
||||
// stack on the right
|
||||
_ => {
|
||||
// Central can be resized left or right
|
||||
if let Some(mut right) = resize_dimensions[0] {
|
||||
if let Some(right) = resize_dimensions[0].as_mut() {
|
||||
right.top = 0;
|
||||
right.bottom = 0;
|
||||
}
|
||||
|
||||
// Left one can only be resized to the right
|
||||
if let Some(mut left) = resize_dimensions[1] {
|
||||
if let Some(left) = resize_dimensions[1].as_mut() {
|
||||
left.top = 0;
|
||||
left.bottom = 0;
|
||||
left.left = 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic-no-console"
|
||||
version = "0.1.39"
|
||||
version = "0.1.40"
|
||||
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2024"
|
||||
@@ -8,5 +8,3 @@ edition = "2024"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.39"
|
||||
version = "0.1.40"
|
||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2024"
|
||||
@@ -18,6 +18,7 @@ dunce = { workspace = true }
|
||||
fs-tail = "0.1"
|
||||
lazy_static = { workspace = true }
|
||||
miette = { version = "7", features = ["fancy"] }
|
||||
open = "5"
|
||||
paste = { workspace = true }
|
||||
powershell_script = "1.0"
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
@@ -28,6 +29,7 @@ shadow-rs = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
thiserror = "2"
|
||||
which = { workspace = true }
|
||||
win-msgbox = "0.2"
|
||||
windows = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
@@ -39,4 +41,4 @@ default = ["schemars"]
|
||||
schemars = ["dep:schemars", "komorebi-client/default"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(FALSE)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(FALSE)'] }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,6 +84,7 @@ nav:
|
||||
- common-workflows/multi-monitor-setup.md
|
||||
- CLI reference:
|
||||
- cli/quickstart.md
|
||||
- cli/license.md
|
||||
- cli/start.md
|
||||
- cli/stop.md
|
||||
- cli/kill.md
|
||||
@@ -110,6 +111,8 @@ nav:
|
||||
- cli/load-resize.md
|
||||
- cli/focus.md
|
||||
- cli/move.md
|
||||
- cli/preselect-direction.md
|
||||
- cli/cancel-preselect.md
|
||||
- cli/minimize.md
|
||||
- cli/close.md
|
||||
- cli/force-focus.md
|
||||
@@ -169,6 +172,7 @@ nav:
|
||||
- cli/scrolling-layout-columns.md
|
||||
- cli/flip-layout.md
|
||||
- cli/promote.md
|
||||
- cli/promote-swap.md
|
||||
- cli/promote-focus.md
|
||||
- cli/promote-window.md
|
||||
- cli/retile.md
|
||||
@@ -255,4 +259,4 @@ nav:
|
||||
- cli/static-config-schema.md
|
||||
- cli/generate-static-config.md
|
||||
- cli/enable-autostart.md
|
||||
- cli/disable-autostart.md
|
||||
- cli/disable-autostart.md
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
channel = "stable"
|
||||
|
||||
@@ -1 +1 @@
|
||||
imports_granularity = "Item"
|
||||
imports_granularity = "Item"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user