Compare commits

..

1 Commits

Author SHA1 Message Date
LGUG2Z
5f44f6e7cc docs(license): switch to polyform, add contributing.md 2024-04-12 14:43:22 -07:00
158 changed files with 4527 additions and 21863 deletions

52
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,52 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]: Short descriptive title"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See bug
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots and Videos**
Add screenshots and videos to help explain your problem.
**Operating System**
Provide the output of `systeminfo | grep "^OS Name\|^OS Version"`
For example:
```
OS Name: Microsoft Windows 11 Pro
OS Version: 10.0.22000 N/A Build 22000
```
**`komorebic check` Output**
Provide the output of `komorebic check`
For example:
```
No KOMOREBI_CONFIG_HOME detected, defaulting to C:\Users\LGUG2Z
Looking for configuration files in C:\Users\LGUG2Z
No komorebi configuration found in C:\Users\LGUG2Z
If running 'komorebic start --await-configuration', you will manually have to call the following command to begin tiling: komorebic complete-configuration
```
**Additional context**
Add any other context about the problem here.
In particular, if you have any other AHK scripts or software running that handle any aspect of window management or manipulation

View File

@@ -1,55 +0,0 @@
name: Bug report
description: File a bug report
labels: [ bug ]
body:
- type: textarea
validations:
required: true
attributes:
label: Summary
description: >
Please provide a short summary of the bug, along with any information
you feel is relevant to replicating the bug.
You may include screenshots and videos in this section.
- type: textarea
validations:
required: true
attributes:
label: Version Information
description: >
Please provide information about the versions of Windows and komorebi
running on your machine.
Do not submit a bug if you are not using an official version of Windows
such as AtlasOS; only official versions of Windows are supported.
```
systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version"
```
```
komorebic --version
```
- type: textarea
validations:
required: true
attributes:
label: Komorebi Configuration
description: >
Please provide your configuration file (komorebi.json or komorebi.bar.json)
render: json
- type: textarea
validations:
required: true
attributes:
label: Hotkey Configuration
description: >
Please provide your whkdrc or komorebi.ahk hotkey configuration file
- type: textarea
validations:
required: true
attributes:
label: Output of komorebic check
description: >
Please provide the output of `komorebic check`

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Komorebi Documentation
url: https://lgug2z.github.io/komorebi/
about: Please search the documentation website before opening an issue

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEAT]: Short descriptive title"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,18 +0,0 @@
name: Feature request
description: Suggest an improvement
labels: [ enhancement ]
body:
- type: textarea
validations:
required: true
attributes:
label: Suggestion
description: >
Please share your suggestion here. Be sure to include all necessary context.
- type: textarea
validations:
required: true
attributes:
label: Alternatives Considered
description: >
Please share share alternatives you have considered here.

View File

@@ -14,16 +14,14 @@ on:
tags:
- v*
schedule:
- cron: "30 0 * * 0" # Every day at 00:30 UTC
- cron: "30 0 * * 1" # Every Monday at half past midnight UTC
jobs:
build:
name: Build
runs-on: windows-latest
permissions: write-all
env:
RUSTFLAGS: -Ctarget-feature=+crt-static
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
matrix:
@@ -95,47 +93,23 @@ jobs:
target/${{ matrix.target }}/release/komorebi.exe
target/${{ matrix.target }}/release/komorebic.exe
target/${{ matrix.target }}/release/komorebic-no-console.exe
target/${{ matrix.target }}/release/komorebi-bar.exe
target/${{ matrix.target }}/release/komorebi-gui.exe
target/${{ matrix.target }}/release/komorebi.pdb
target/${{ matrix.target }}/release/komorebic.pdb
target/${{ matrix.target }}/release/komorebi_gui.pdb
target/${{ matrix.target }}/release/komorebic-no-console.pdb
target/wix/komorebi-*.msi
retention-days: 7
- name: Check GoReleaser
uses: goreleaser/goreleaser-action@v3
env:
GORELEASER_CURRENT_TAG: v0.1.29
with:
version: latest
args: build --skip=validate --clean
- name: Prepare nightly artifacts
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
run: |
Compress-Archive .\target\${{ matrix.target }}\release\*.exe komorebi-nightly-x86_64-pc-windows-msvc.zip
Copy-Item ./target/wix/*.msi -Destination ./komorebi-nightly-x86_64.msi
echo "$((Get-FileHash komorebi-nightly-x86_64-pc-windows-msvc.zip).Hash.ToLower()) komorebi-nightly-x86_64-pc-windows-msvc.zip" >checksums.txt
- name: Update nightly
if: ${{ github.ref == 'refs/heads/master' && github.event_name == 'schedule' }}
shell: bash
run: |
gh release delete nightly --yes || true
git push origin :nightly || true
gh release create nightly \
--target $GITHUB_SHA \
--prerelease \
--title "komorebi nightly (${GITHUB_SHA})" \
--notes "This nightly release of komorebi corresponds to [this commit](https://github.com/LGUG2Z/komorebi/commit/${GITHUB_SHA})." \
komorebi-nightly-x86_64-pc-windows-msvc.zip \
komorebi-nightly-x86_64.msi \
checksums.txt
# Release
- name: Generate changelog
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
run: |
if ! type kokai >/dev/null; then cargo install --locked kokai --force; fi
git tag -d nightly
kokai release --no-emoji --add-links github:commits,issues --ref "$(git tag --points-at HEAD)" >"CHANGELOG.md"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3

4
.gitignore vendored
View File

@@ -3,5 +3,5 @@
/target
CHANGELOG.md
dummy.go
komorebic/applications.yaml
/.vs
komorebi.ahk
komorebic/applications.yaml

View File

@@ -10,8 +10,8 @@ before:
builds:
- id: komorebi
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
goos: ["windows"]
goarch: ["amd64"]
binary: komorebi
hooks:
post:
@@ -19,8 +19,8 @@ builds:
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi.exe" ".\dist\komorebi_windows_amd64_v1\komorebi.exe"
- id: komorebic
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
goos: ["windows"]
goarch: ["amd64"]
binary: komorebic
hooks:
post:
@@ -28,37 +28,19 @@ builds:
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic.exe" ".\dist\komorebic_windows_amd64_v1\komorebic.exe"
- id: komorebic-no-console
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
goos: ["windows"]
goarch: ["amd64"]
binary: komorebic-no-console
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebic-no-console.exe" ".\dist\komorebic-no-console_windows_amd64_v1\komorebic-no-console.exe"
- id: komorebi-gui
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi-gui
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-gui.exe" ".\dist\komorebi-gui_windows_amd64_v1\komorebi-gui.exe"
- id: komorebi-bar
main: dummy.go
goos: [ "windows" ]
goarch: [ "amd64" ]
binary: komorebi-bar
hooks:
post:
- mkdir -p dist/windows_amd64
- cp ".\target\x86_64-pc-windows-msvc\release\komorebi-bar.exe" ".\dist\komorebi-bar_windows_amd64_v1\komorebi-bar.exe"
archives:
- name_template: "{{ .ProjectName }}-{{ .Version }}-x86_64-pc-windows-msvc"
format: zip
files:
- LICENSE.md
- LICENSE
- CHANGELOG.md
checksum:

4702
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,46 +2,24 @@
resolver = "2"
members = [
"derive-ahk",
"komorebi",
"komorebi-client",
"komorebi-gui",
"komorebi-core",
"komorebic",
"komorebic-no-console",
"komorebi-bar",
"komorebi-themes"
]
[workspace.dependencies]
clap = { version = "4", features = ["derive", "wrap_help"] }
chrono = "0.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
color-eyre = "0.6"
eframe = "0.29"
egui_extras = "0.29"
dirs = "5"
windows-interface = { version = "0.53" }
windows-implement = { version = "0.53" }
dunce = "1"
hotwatch = "0.5"
schemars = "0.8"
lazy_static = "1"
serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" }
serde_yaml = "0.9"
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
paste = "1"
sysinfo = "0.31"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "dd65e3f22d0521b78fcddde11abc2a3e9dcc32a8" }
windows-implement = { version = "0.58" }
windows-interface = { version = "0.58" }
windows-core = { version = "0.58" }
shadow-rs = "0.35"
which = "6"
dirs = "5"
color-eyre = "0.6"
serde_json = { package = "serde_json_lenient", version = "0.1" }
[workspace.dependencies.windows]
version = "0.58"
version = "0.54"
features = [
"implement",
"Win32_System_Com",
@@ -58,11 +36,5 @@ features = [
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_SystemServices",
"Win32_System_WindowsProgramming",
"Media",
"Media_Control"
"Win32_System_SystemServices"
]
[profile.release]
lto = true

View File

@@ -1,6 +1,6 @@
# Komorebi License
# PolyForm Strict License 1.0.0
Version 1.0.0
<https://polyformproject.org/licenses/strict/1.0.0>
## Acceptance
@@ -13,14 +13,8 @@ your licenses.
The licensor grants you a copyright license for the software
to do everything you might do with the software that would
otherwise infringe the licensor's copyright in it for any
permitted purpose. However, you may only make changes according
to the [Changes License](#changes-license), and you may not
distribute the software or new works based on the software.
## Changes License
The licensor grants you an additional copyright license to
make changes for any permitted purpose.
permitted purpose, other than distributing the software or
making changes or new works based on the software.
## Patent License
@@ -28,6 +22,10 @@ The licensor grants you a patent license for the software that
covers patent claims the licensor can license, or becomes able
to license, that you would infringe by using the software.
## Noncommercial Purposes
Any noncommercial purpose is a permitted purpose.
## Personal Uses
Personal use for research, experiment, and testing for
@@ -36,6 +34,15 @@ entertainment, hobby projects, amateur pursuits, or religious
observance, without any anticipated commercial application,
is use for a permitted purpose.
## Noncommercial Organizations
Use by any charitable organization, educational institution,
public research organization, public safety or health
organization, environmental protection organization,
or government institution is use for a permitted purpose
regardless of the source of funding or obligations resulting
from the funding.
## Fair Use
You may have "fair use" rights for the software under the

View File

@@ -1,8 +0,0 @@
# Privacy Policy for Komorebi
No data about your device(s) or _komorebi_ usage leave your device.
## Data Maintained by Komorebi
_komorebi_ writes log files to and keeps a list of temporary window handles (HWNDs) currently managed by the process in
the `$Env:LOCALAPPDATA\komorebi\` directory. This directory is owned by the user running the process.

View File

@@ -60,7 +60,7 @@ channel](https://www.youtube.com/channel/UCeai3-do-9O4MNy9_xjO6mg) where I post
_komorebi_ development videos. If you would like to be notified of upcoming
videos please subscribe and turn on notifications.
_komorebi_ is a free and source-available project, and one that encourages you to
_komorebi_ is a free and open-source project, and one that encourages you to
make charitable donations if you find the software to be useful and have the
financial means.
@@ -82,30 +82,10 @@ A [detailed installation and quickstart
guide](https://lgug2z.github.io/komorebi/installation.html) is available which shows how to get started
using `scoop`, `winget` or building from source.
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/MMZUAtHbTYY/hqdefault.jpg)](https://www.youtube.com/watch?v=MMZUAtHbTYY)
# Comparison With Fancy Zones
Community member [Olge](https://www.youtube.com/@polle5555) has created an
excellent video which compares the default window management features of
Windows 11, Fancy Zones and komorebi.
If you are not familiar with tiling window managers or if you are looking at
komorebi and wondering "how is this different from Fancy Zones? 🤔", this short
video will answer the majority of your questions.
[![Watch the comparison video](https://img.youtube.com/vi/0LCbS_gm0RA/hqdefault.jpg)](https://www.youtube.com/watch?v=0LCbS_gm0RA)
[![Watch the quickstart walkthrough video](https://img.youtube.com/vi/H9-_c1egQ4g/hqdefault.jpg)](https://www.youtube.com/watch?v=H9-_c1egQ4g)
# Demonstrations
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
unfocused window transparency and animations enabled, using a custom status bar integrated using
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
[@haxibami](https://github.com/haxibami) showing _komorebi_ running on Windows
11 with a terminal emulator, a web browser and a code editor. The original
video can be viewed
@@ -191,8 +171,7 @@ required.
## License
`komorebi` is licensed under the [Komorebi 1.0.0 license](./LICENSE.md), which
is a fork of the [PolyForm Strict 1.0.0
`komorebi` is licensed under the [PolyForm Strict 1.0.0
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
this means that you are free to do whatever you want with `komorebi` other than
redistribution, or distribution of new works (ie. hard-forks) based on the
@@ -280,7 +259,7 @@ If the named pipe exists, `komorebi` will start pushing JSON data of successfull
You may then filter on the `type` key to listen to the events that you are interested in. For a full list of possible
notification types, refer to the enum variants of `WindowManagerEvent` in `komorebi` and `SocketMessage`
in `komorebi::core`.
in `komorebi-core`.
Below is an example of how you can subscribe to and filter on events using a named pipe in `nodejs`.
@@ -359,7 +338,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.29"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.24"}
use anyhow::Result;
use komorebi_client::Notification;
@@ -415,7 +394,7 @@ A TCP listener can optionally be exposed on a port of your choosing with the `--
provided to `komorebi` or `komorebic start`, no TCP listener will be created.
Once created, your client may send
any [SocketMessage](https://github.com/LGUG2Z/komorebi/blob/master/komorebi/src/core/mod.rs#L37) to `komorebi` in the
any [SocketMessage](https://github.com/LGUG2Z/komorebi/blob/master/komorebi-core/src/lib.rs#L37) to `komorebi` in the
same way that `komorebic` would.
This can be used if you would like to create your own alternative to `komorebic` which incorporates scripting and

14
derive-ahk/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "derive-ahk"
version = "0.1.1"
edition = "2021"
[lib]
proc-macro = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "1.0"
syn = "1.0"
quote = "1.0"

225
derive-ahk/src/lib.rs Normal file
View File

@@ -0,0 +1,225 @@
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![no_implicit_prelude]
use ::std::clone::Clone;
use ::std::convert::From;
use ::std::convert::Into;
use ::std::format;
use ::std::iter::Extend;
use ::std::iter::Iterator;
use ::std::matches;
use ::std::option::Option::Some;
use ::std::string::String;
use ::std::string::ToString;
use ::std::unreachable;
use ::std::vec::Vec;
use ::quote::quote;
use ::syn::parse_macro_input;
use ::syn::Data;
use ::syn::DataEnum;
use ::syn::DeriveInput;
use ::syn::Fields;
use ::syn::FieldsNamed;
use ::syn::FieldsUnnamed;
use ::syn::Meta;
use ::syn::NestedMeta;
#[allow(clippy::too_many_lines)]
#[proc_macro_derive(AhkFunction)]
pub fn ahk_function(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
match input.data {
Data::Struct(s) => match s.fields {
Fields::Named(FieldsNamed { named, .. }) => {
let argument_idents = named
.iter()
// Filter out the flags
.filter(|&f| {
let mut include = true;
for attribute in &f.attrs {
if let ::std::result::Result::Ok(Meta::List(list)) =
attribute.parse_meta()
{
for nested in list.nested {
if let NestedMeta::Meta(Meta::Path(path)) = nested {
if path.is_ident("long") {
include = false;
}
}
}
}
}
include
})
.map(|f| &f.ident);
let argument_idents_clone = argument_idents.clone();
let called_arguments = quote! {#(%#argument_idents_clone%) *}
.to_string()
.replace(" %", "%")
.replace("% ", "%")
.replace("%%", "% %");
let flag_idents = named
.iter()
// Filter only the flags
.filter(|f| {
let mut include = false;
for attribute in &f.attrs {
if let ::std::result::Result::Ok(Meta::List(list)) =
attribute.parse_meta()
{
for nested in list.nested {
if let NestedMeta::Meta(Meta::Path(path)) = nested {
// Identify them using the --long flag name
if path.is_ident("long") {
include = true;
}
}
}
}
}
include
})
.map(|f| &f.ident);
let has_flags = flag_idents.clone().count() != 0;
if has_flags {
let flag_idents_concat = flag_idents.clone();
let argument_idents_concat = argument_idents.clone();
// Concat the args and flag args if there are flags
let all_arguments =
quote! {#(#argument_idents_concat,) * #(#flag_idents_concat), *}
.to_string();
let flag_idents_clone = flag_idents.clone();
let flags = quote! {#(--#flag_idents_clone) *}
.to_string()
.replace("- - ", "--")
.replace('_', "-");
let called_flag_arguments = quote! {#(%#flag_idents%) *}
.to_string()
.replace(" %", "%")
.replace("% ", "%")
.replace("%%", "% %");
let flags_split: Vec<_> = flags.split(' ').collect();
let flag_args_split: Vec<_> = called_flag_arguments.split(' ').collect();
let mut consolidated_flags: Vec<String> = Vec::new();
for (idx, flag) in flags_split.iter().enumerate() {
consolidated_flags.push(format!("{} {}", flag, flag_args_split[idx]));
}
let all_flags = consolidated_flags.join(" ");
quote! {
impl AhkFunction for #name {
fn generate_ahk_function() -> String {
::std::format!(r#"
{}({}) {{
RunWait, komorebic.exe {} {} {}, , Hide
}}"#,
::std::stringify!(#name),
#all_arguments,
::std::stringify!(#name).to_kebab_case(),
#called_arguments,
#all_flags,
)
}
}
}
} else {
let arguments = quote! {#(#argument_idents), *}.to_string();
quote! {
impl AhkFunction for #name {
fn generate_ahk_function() -> String {
::std::format!(r#"
{}({}) {{
RunWait, komorebic.exe {} {}, , Hide
}}"#,
::std::stringify!(#name),
#arguments,
::std::stringify!(#name).to_kebab_case(),
#called_arguments
)
}
}
}
}
}
_ => unreachable!("only to be used on structs with named fields"),
},
_ => unreachable!("only to be used on structs"),
}
.into()
}
#[proc_macro_derive(AhkLibrary)]
pub fn ahk_library(input: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
match input.data {
Data::Enum(DataEnum { variants, .. }) => {
let enums = variants.iter().filter(|&v| {
matches!(v.fields, Fields::Unit) || matches!(v.fields, Fields::Unnamed(..))
});
let mut stream = ::proc_macro2::TokenStream::new();
for variant in enums.clone() {
match &variant.fields {
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
for field in unnamed {
stream.extend(quote! {
v.push(#field::generate_ahk_function());
});
}
}
Fields::Unit => {
let name = &variant.ident;
stream.extend(quote! {
v.push(::std::format!(r#"
{}() {{
RunWait, komorebic.exe {}, , Hide
}}"#,
::std::stringify!(#name),
::std::stringify!(#name).to_kebab_case()
));
});
}
Fields::Named(_) => {
unreachable!("only to be used with unnamed and unit fields");
}
}
}
quote! {
impl #name {
fn generate_ahk_library() -> String {
let mut v: Vec<String> = vec![String::from("; Generated by komorebic.exe")];
#stream
v.join("\n")
}
}
}
}
_ => unreachable!("only to be used on enums"),
}
.into()
}

View File

@@ -0,0 +1,26 @@
# active-window-border-colour
```
Set the colour for the active window border
Usage: komorebic.exe active-window-border-colour [OPTIONS] <R> <G> <B>
Arguments:
<R>
Red
<G>
Green
<B>
Blue
Options:
-w, --window-kind <WINDOW_KIND>
[default: single]
[possible values: single, stack, monocle]
-h, --help
Print help
```

View File

@@ -0,0 +1,16 @@
# active-window-border-offset
```
Set the offset for the active window border
Usage: komorebic.exe active-window-border-offset <OFFSET>
Arguments:
<OFFSET>
Desired offset of the active window border
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,16 @@
# active-window-border-width
```
Set the width for the active window border
Usage: komorebic.exe active-window-border-width <WIDTH>
Arguments:
<WIDTH>
Desired width of the active window border
Options:
-h, --help
Print help
```

View File

@@ -1,9 +1,9 @@
# border
# active-window-border
```
Enable or disable borders
Enable or disable the active window border
Usage: komorebic.exe border <BOOLEAN_STATE>
Usage: komorebic.exe active-window-border <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>

12
docs/cli/ahk-library.md Normal file
View File

@@ -0,0 +1,12 @@
# ahk-library
```
Generate a library of AutoHotKey helper functions
Usage: komorebic.exe ahk-library
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# animation-duration
```
Set the duration for movement animations in ms
Usage: komorebic.exe animation-duration <DURATION>
Arguments:
<DURATION>
Desired animation durations in ms
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# animation-fps
```
Set the frames per second for movement animations
Usage: komorebic.exe animation-fps <FPS>
Arguments:
<FPS>
Desired animation frames per second
Options:
-h, --help
Print help
```

View File

@@ -1,20 +0,0 @@
# animation-style
```
Set the ease function for movement animations
Usage: komorebic.exe animation-style [OPTIONS]
Options:
-s, --style <STYLE>
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]
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# animation
```
Enable or disable movement animations
Usage: komorebic.exe animation <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
Options:
-h, --help
Print help
```

View File

@@ -1,12 +0,0 @@
# bar-configuration
```
Show the path to komorebi.bar.json
Usage: komorebic.exe bar-configuration
Options:
-h, --help
Print help
```

View File

@@ -1,26 +0,0 @@
# border-colour
```
Set the colour for a window border kind
Usage: komorebic.exe border-colour [OPTIONS] <R> <G> <B>
Arguments:
<R>
Red
<G>
Green
<B>
Blue
Options:
-w, --window-kind <WINDOW_KIND>
[default: single]
[possible values: single, stack, monocle, unfocused]
-h, --help
Print help
```

View File

@@ -1,20 +0,0 @@
# border-implementation
```
Set the border implementation
Usage: komorebic.exe border-implementation <STYLE>
Arguments:
<STYLE>
Desired border implementation
Possible values:
- komorebi: Use the adjustable komorebi border implementation
- windows: Use the thin Windows accent border implementation
Options:
-h, --help
Print help (see a summary with '-h')
```

View File

@@ -1,16 +0,0 @@
# border-offset
```
Set the border offset
Usage: komorebic.exe border-offset <OFFSET>
Arguments:
<OFFSET>
Desired offset of the window border
Options:
-h, --help
Print help
```

View File

@@ -1,21 +0,0 @@
# border-style
```
Set the border style
Usage: komorebic.exe border-style <STYLE>
Arguments:
<STYLE>
Desired border style
Possible values:
- system: Use the system border style
- rounded: Use the Windows 11-style rounded borders
- square: Use the Windows 10-style square borders
Options:
-h, --help
Print help (see a summary with '-h')
```

View File

@@ -1,16 +0,0 @@
# border-width
```
Set the border width
Usage: komorebic.exe border-width <WIDTH>
Arguments:
<WIDTH>
Desired width of the window border
Options:
-h, --help
Print help
```

View File

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

View File

@@ -1,12 +0,0 @@
# clear-all-workspace-rules
```
Remove all application association rules for all workspaces
Usage: komorebic.exe clear-all-workspace-rules
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# clear-named-workspace-rules
```
Remove all application association rules for a named workspace
Usage: komorebic.exe clear-named-workspace-rules <WORKSPACE>
Arguments:
<WORKSPACE>
Name of a workspace
Options:
-h, --help
Print help
```

View File

@@ -1,19 +0,0 @@
# clear-workspace-rules
```
Remove all application association rules for a workspace by monitor and workspace index
Usage: komorebic.exe clear-workspace-rules <MONITOR> <WORKSPACE>
Arguments:
<MONITOR>
Monitor index (zero-indexed)
<WORKSPACE>
Workspace index on the specified monitor (zero-indexed)
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# complete-configuration
```
For legacy komorebi.ahk or komorebi.ps1 configurations, signal that the final configuration option has been sent
Signal that the final configuration option has been sent
Usage: komorebic.exe complete-configuration

View File

@@ -10,7 +10,6 @@ Arguments:
Possible values:
- swap: Swap the window container with the window container at the edge of the adjacent monitor
- insert: Insert the window container into the focused workspace on the adjacent monitor
- no-op: Do nothing if trying to move a window container in the direction of an adjacent monitor
Options:
-h, --help

View File

@@ -1,16 +0,0 @@
# cycle-move-workspace-to-monitor
```
Move the focused workspace monitor in the given cycle direction
Usage: komorebic.exe cycle-move-workspace-to-monitor <CYCLE_DIRECTION>
Arguments:
<CYCLE_DIRECTION>
[possible values: previous, next]
Options:
-h, --help
Print help
```

View File

@@ -18,9 +18,6 @@ Options:
--ahk
Enable autostart of ahk
--bar
Enable autostart of komorebi-bar
-h, --help
Print help

View File

@@ -1,16 +0,0 @@
# focus-stack-window
```
Focus the specified window index in the focused stack
Usage: komorebic.exe focus-stack-window <TARGET>
Arguments:
<TARGET>
Target index (zero-indexed)
Options:
-h, --help
Print help
```

View File

@@ -1,12 +0,0 @@
# global-state
```
Show a JSON representation of the current global state
Usage: komorebic.exe global-state
Options:
-h, --help
Print help
```

View File

@@ -1,12 +0,0 @@
# gui
```
Launch the komorebi-gui debugging tool
Usage: komorebic.exe gui
Options:
-h, --help
Print help
```

View File

@@ -1,12 +0,0 @@
# monitor-information
```
Show information about connected monitors
Usage: komorebic.exe monitor-information
Options:
-h, --help
Print help
```

View File

@@ -1,19 +0,0 @@
# move-to-monitor-workspace
```
Move the focused window to the specified monitor workspace
Usage: komorebic.exe move-to-monitor-workspace <TARGET_MONITOR> <TARGET_WORKSPACE>
Arguments:
<TARGET_MONITOR>
Target monitor index (zero-indexed)
<TARGET_WORKSPACE>
Workspace index on the target monitor (zero-indexed)
Options:
-h, --help
Print help
```

View File

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

View File

@@ -10,7 +10,7 @@ Arguments:
Target workspace name
<VALUE>
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid]
Options:
-h, --help

View File

@@ -1,16 +0,0 @@
# promote-window
```
Promote the window in the specified direction
Usage: komorebic.exe promote-window <OPERATION_DIRECTION>
Arguments:
<OPERATION_DIRECTION>
[possible values: left, right, up, down]
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# reload-configuration
```
Reload legacy komorebi.ahk or komorebi.ps1 configurations (if they exist)
Reload ~/komorebi.ahk (if it exists)
Usage: komorebic.exe reload-configuration

View File

@@ -1,16 +0,0 @@
# replace-configuration
```
Replace the configuration of a running instance of komorebi from a static configuration file
Usage: komorebic.exe replace-configuration <PATH>
Arguments:
<PATH>
Static configuration JSON file from which the configuration should be loaded
Options:
-h, --help
Print help
```

View File

@@ -1,12 +0,0 @@
# stack-all
```
Stack all windows on the focused workspace
Usage: komorebic.exe stack-all
Options:
-h, --help
Print help
```

View File

@@ -24,9 +24,6 @@ Options:
--ahk
Start autohotkey configuration file
--bar
Start komorebi-bar in a background process
-h, --help
Print help

View File

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

View File

@@ -1,12 +0,0 @@
# toggle-transparency
```
Toggle transparency for unfocused windows
Usage: komorebic.exe toggle-transparency
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# transparency-alpha
```
Set the alpha value for unfocused window transparency
Usage: komorebic.exe transparency-alpha <ALPHA>
Arguments:
<ALPHA>
Alpha
Options:
-h, --help
Print help
```

View File

@@ -1,16 +0,0 @@
# transparency
```
Enable or disable transparency for unfocused windows
Usage: komorebic.exe transparency <BOOLEAN_STATE>
Arguments:
<BOOLEAN_STATE>
[possible values: enable, disable]
Options:
-h, --help
Print help
```

View File

@@ -1,12 +0,0 @@
# unstack-all
```
Unstack all windows in the focused container
Usage: komorebic.exe unstack-all
Options:
-h, --help
Print help
```

View File

@@ -1,7 +1,7 @@
# watch-configuration
```
Enable or disable watching of legacy komorebi.ahk or komorebi.ps1 configurations (if they exist)
Enable or disable watching of ~/komorebi.ahk (if it exists)
Usage: komorebic.exe watch-configuration <BOOLEAN_STATE>

View File

@@ -10,7 +10,7 @@ Arguments:
Possible values:
- hide: Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
- minimize: Use the SW_MINIMIZE flag to hide windows when switching workspaces (has issues with frequent workspace switching)
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces
- cloak: Use the undocumented SetCloak Win32 function to hide windows when switching workspaces (has foregrounding issues)
Options:
-h, --help

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
# Active Window Border
If you would like to add a visual border around the currently focused window,
ensure the following options are defined in the `komorebi.json` configuration
file.
```json
{
"active_window_border": true,
"active_window_border_style": "Rounded",
"active_window_border_colours": {
"single": {
"r": 66,
"g": 165,
"b": 245
},
"stack": {
"r": 256,
"g": 165,
"b": 66
},
"monocle": {
"r": 255,
"g": 51,
"b": 153
}
},
}
```
It is important to note that the active window border will only apply to
windows managed by `komorebi`.
This feature is not considered stable, and you may encounter visual artifacts
from time to time.
[![Watch the tutorial
video](https://img.youtube.com/vi/7_9D22t7KK4/hqdefault.jpg)](https://www.youtube.com/watch?v=7_9D22t7KK4)

View File

@@ -1,25 +0,0 @@
# Animations
If you would like to add window movement animations, ensure the following options are
defined in the `komorebi.json` configuration file.
```json
{
"animation": {
"enabled": true
}
}
```
Window movement animations only apply to actions taking place within the same monitor
workspace.
You can optionally set a custom duration in ms with `animation.duration` (default: `250`),
a custom style with `animation.style` (default: `Linear`), and a custom FPS value with
`animation.fps` (default: `60`).
It is important to note that higher `fps` and a longer `duration` settings will result
in increased CPU usage.
This feature is not considered stable, and you may encounter visual artifacts
from time to time.

View File

@@ -1,4 +1,6 @@
# AutoHotkey
# AutoHotKey
<!-- TODO: Update this completely -->
If you would like to use Autohotkey, please make sure you have AutoHotKey v2
installed.
@@ -7,16 +9,22 @@ Generally, users who opt for AHK will have specific needs that can only be
addressed by the advanced functionality of AHK, and so they are assumed to be
able to craft their own configuration files.
If you would like to try out AHK, here is a simple sample configuration which
largely matches the `whkdrc` sample configuration.
If you would like to try out AHK, a simple sample configuration powered by
`komorebic.lib.ahk` is provided as a starting point. This sample configuration
does not take into account the use of a static configuration file; if you
choose to use a static configuration file alongside AHK, you can remove all the
configuration options from your `komorebi.ahk` and use it solely to handle
hotkey bindings.
```autohotkey
{% include "./komorebi.ahk.txt" %}
```powershell
# save the latest generated komorebic library to ~/komorebic.lib.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebic.lib.ahk -OutFile $Env:USERPROFILE\komorebic.lib.ahk
# save the latest generated app-specific config tweaks and fixes to ~/komorebi.generated.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebi.generated.ahk -OutFile $Env:USERPROFILE\komorebi.generated.ahk
# save the sample komorebi configuration file to ~/komorebi.ahk
iwr https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.20/komorebi.sample.ahk -OutFile $Env:USERPROFILE\komorebi.ahk
```
By default, the `komorebi.ahk` file should be located in the `$Env:USERPROFILE`
directory, however, if `$Env:KOMOREBI_CONFIG_HOME` is set, it should be located
there.
Once the file is in place, you can stop komorebi and whkd by running `komorebic stop --whkd`,
and then start komorebi with Autohotkey by running `komorebic start --ahk`.

View File

@@ -1,28 +0,0 @@
# Borders
If you would like to add a visual border around both the currently focused window
and unfocused windows ensure the following options are defined in the `komorebi.json`
configuration file.
```json
{
"border": true,
"border_width": 8,
"border_offset": -1,
"border_style": "System",
"border_colours": {
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399",
"unfocused": "#808080"
}
}
```
It is important to note that borders will only apply to windows managed by `komorebi`.
This feature is not considered stable, and you may encounter visual artifacts
from time to time.
[![Watch the tutorial
video](https://img.youtube.com/vi/7_9D22t7KK4/hqdefault.jpg)](https://www.youtube.com/watch?v=7_9D22t7KK4)

View File

@@ -1,4 +1,4 @@
# Dynamic Layout Switching
# Dynamically Layout Switching
With `komorebi` it is possible to define rules to automatically change the
layout on a specified workspace when a threshold of window containers is met.

View File

@@ -2,7 +2,7 @@
❗️**NOTE**: A significant number of force-manage window rules for the most
common applications are [already generated for
you](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
you](https://github.com/LGUG2Z/komorebi/#generating-common-application-specific-configurations)
In some rare cases, a window may not automatically be registered to be managed
by `komorebi`. You can add rules to enforce this behaviour in the

View File

@@ -2,7 +2,7 @@
❗️**NOTE**: A significant number of ignored window rules for the most common
applications are [already generated for
you](https://github.com/LGUG2Z/komorebi-application-specific-configuration)
you](https://github.com/LGUG2Z/komorebi/#generating-common-application-specific-configurations)
Sometimes you will want a specific application to never be tiled, and instead
float all the time. You can add rules to enforce this behaviour in the

View File

@@ -1,17 +0,0 @@
# Setting a Given Display to a Specific Index
If you would like `komorebi` to remember monitor index positions, you will need to set the `display_index_preferences`
configuration option in the static configuration file.
Display IDs can be found using `komorebic monitor-information`.
Then, in `komorebi.json`, you simply need to specify the preferred index position for each display ID:
```json
{
"display_index_preferences": {
"0": "DEL4310-5&1a6c0954&0&UID209155",
"1": "<another-display_id>"
}
}
```

View File

@@ -16,17 +16,16 @@ the example files have been downloaded. For most new users this will be in the
komorebic quickstart
```
With the example configurations downloaded, you can now start `komorebi`,
`komorebi-bar` and `whkd`.
With the example configurations downloaded, you can now start `komorebi` and `whkd.
```powershell
komorebic start --whkd --bar
komorebic start --whkd
```
## komorebi.json
The example window manager configuration sets some sane defaults and provides
seven preconfigured workspaces on the primary monitor each with a different
five preconfigured workspaces on the primary monitor each with a different
layout.
```json
@@ -102,16 +101,6 @@ monocle.
+-------+-----+
```
#### RightMainVerticalStack
```
+-----+-------+
| | |
+-----+ |
| | |
+-----+-------+
```
#### Horizontal Stack
```
@@ -133,7 +122,6 @@ monocle.
```
#### Rows
If you have a vertical monitor, I recommend using this layout.
```
@@ -145,7 +133,6 @@ If you have a vertical monitor, I recommend using this layout.
```
#### Ultrawide Vertical Stack
If you have an ultrawide monitor, I recommend using this layout.
```
@@ -159,7 +146,6 @@ If you have an ultrawide monitor, I recommend using this layout.
```
### Grid
If you like the `grid` layout in [LeftWM](https://github.com/leftwm/leftwm-layouts) this is almost exactly the same!
```
@@ -197,8 +183,7 @@ which shell you use in your terminal.
* `powershell` - set this if you are using the version of PowerShell that comes
installed with Windows 10+ (the executable file for this is `powershell.exe`)
* `pwsh` - set this if you are using PowerShell 7+, which you have installed yourself either through the Windows Store
or WinGet (the executable file for this is `pwsh.exe`)
* `pwsh` - set this if you are using PowerShell 7+, which you have installed yourself either through the Windows Store or WinGet (the executable file for this is `pwsh.exe`)
* `cmd` - set this if you don't want to use PowerShell at all and instead you
want to call commands through the shell used by the old-school Command
@@ -214,24 +199,3 @@ reference.
If you want to use one of those key codes, put them into lower case and remove
the `VK_` prefix. For example, the keycode `VK_OEM_PLUS` becomes `oem_plus` in
the sample configuration above.
## komorebi.bar.json
The example status bar configuration sets some sane defaults and provides
a number of pre-configured widgets on the primary monitor.
```json
{% include "./komorebi.bar.example.json" %}
```
### Themes
Themes can be set in either `komorebi.json` or `komorebi.bar.json`. If set
in `komorebi.json`, the theme will be applied to both komorebi's borders and
stackbars as well as the status bar.
If set in `komorebi.bar.json`, the theme will only be applied to the status bar.
All [Catppuccin palette variants](https://catppuccin.com/)
and [most Base16 palette variants](https://tinted-theming.github.io/base16-gallery/)
are available as themes.

View File

@@ -1,11 +1,11 @@
# Getting started
`komorebi` is a tiling window manager for Windows that is comprised of two
main binaries, `komorebi.exe`, which contains the window manager itself,
`komorebi` is a tiling window manager for Windows that is comprised comprised
of two main binaries, `komorebi.exe`, which contains the window manager itself,
and `komorebic.exe`, which is the main way to send commands to the tiling
window manager.
It is important to note that neither `komorebi.exe` nor `komorebic.exe` handle
It is important to note that neither `komorebi.exe` or `komorebic.exe` handle
key bindings, because `komorebi` is a tiling window manager and not a hotkey
daemon.
@@ -23,25 +23,21 @@ suggest that once you are familiar with the main `komorebic.exe` commands used
to manipulate the window manager, you use
[AutoHotKey](https://www.autohotkey.com/) to handle your key bindings.
`komorebi` also includes `komorebi-bar.exe`, a simple and reliable status bar which
is deeply integrated with the tiling window manager, and can be customized with
various widgets and themes.
## Installation
`komorebi` is available pre-built to install via
[Scoop](https://scoop.sh/#/apps?q=komorebi) and
[WinGet](https://winget.run/pkg/LGUG2Z/komorebi), and you may also build
[WinGet](https://winget.run/pkg/LGUG2Z/komorebi), and you may also built
it from [source](https://github.com/LGUG2Z/komorebi) if you would prefer.
- [Scoop](#scoop)
- [WinGet](#winget)
- [Building from source](#building-from-source)
- [Offline](#offline)
- [Scoop](#scoop)
- [WinGet](#winget)
- [Building from source](#building-from-source)
- [Offline](#offline)
## Long path support
It is highly recommended that you enable support for long paths in Windows by
It highly recommended that you enable support for long paths in Windows by
running the following command in an Administrator Terminal before installing
`komorebi`.
@@ -49,12 +45,6 @@ running the following command in an Administrator Terminal before installing
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
```
## Disabling unnecessary system animations
It is highly recommended that you enable the "Turn off all unnecessary animations (when possible)" option in
"Control Panel > Ease of Access > Ease of Access Centre / Make the computer easier to see" for the best performance with
komorebi.
## Scoop
Make sure you have installed [`scoop`](https://scoop.sh) and verified that
@@ -118,8 +108,6 @@ Clone the git repository, enter the directory, and build the following binaries:
cargo +stable install --path komorebi --locked
cargo +stable install --path komorebic --locked
cargo +stable install --path komorebic-no-console --locked
cargo +stable install --path komorebi-gui --locked
cargo +stable install --path komorebi-bar --locked
```
If the binaries have been built and added to your `$PATH` correctly, you should
@@ -133,21 +121,3 @@ an offline machine to install.
Once installed, proceed to get the [example configurations](example-configurations.md) (none of the commands for
first-time set up and running komorebi require an internet connection).
## Uninstallation
Before uninstalling, first run `komorebic stop --whkd --bar` to make sure that
the `komorebi`, `komorebi-bar` and `whkd` processes have been stopped.
Then, depending on whether you installed with Scoop or WinGet, run `scoop
uninstall komorebi whkd` or `winget uninstall LGUG2Z.komorebi LGUG2Z.whkd`.
Finally, you can run the following commands in a PowerShell prompt to clean up
files created by the `quickstart` command and any other runtime files:
```powershell
rm $Env:USERPROFILE\komorebi.json
rm $Env:USERPROFILE\applications.yaml
rm $Env:USERPROFILE\.config\whkdrc
rm -r -Force $Env:LOCALAPPDATA\komorebi
```

View File

@@ -1,71 +0,0 @@
#Requires AutoHotkey v2.0.2
#SingleInstance Force
Komorebic(cmd) {
RunWait(format("komorebic.exe {}", cmd), , "Hide")
}
!q::Komorebic("close")
!m::Komorebic("minimize")
; Focus windows
!h::Komorebic("focus left")
!j::Komorebic("focus down")
!k::Komorebic("focus up")
!l::Komorebic("focus right")
!+[::Komorebic("cycle-focus previous")
!+]::Komorebic("cycle-focus next")
; Move windows
!+h::Komorebic("move left")
!+j::Komorebic("move down")
!+k::Komorebic("move up")
!+l::Komorebic("move right")
; Stack windows
!Left::Komorebic("stack left")
!Down::Komorebic("stack down")
!Up::Komorebic("stack up")
!Right::Komorebic("stack right")
!;::Komorebic("unstack")
![::Komorebic("cycle-stack previous")
!]::Komorebic("cycle-stack next")
; Resize
!=::Komorebic("resize-axis horizontal increase")
!-::Komorebic("resize-axis horizontal decrease")
!+=::Komorebic("resize-axis vertical increase")
!+_::Komorebic("resize-axis vertical decrease")
; Manipulate windows
!t::Komorebic("toggle-float")
!f::Komorebic("toggle-monocle")
; Window manager options
!+r::Komorebic("retile")
!p::Komorebic("toggle-pause")
; Layouts
!x::Komorebic("flip-layout horizontal")
!y::Komorebic("flip-layout vertical")
; Workspaces
!1::Komorebic("focus-workspace 0")
!2::Komorebic("focus-workspace 1")
!3::Komorebic("focus-workspace 2")
!4::Komorebic("focus-workspace 3")
!5::Komorebic("focus-workspace 4")
!6::Komorebic("focus-workspace 5")
!7::Komorebic("focus-workspace 6")
!8::Komorebic("focus-workspace 7")
; Move windows across workspaces
!+1::Komorebic("move-to-workspace 0")
!+2::Komorebic("move-to-workspace 1")
!+3::Komorebic("move-to-workspace 2")
!+4::Komorebic("move-to-workspace 3")
!+5::Komorebic("move-to-workspace 4")
!+6::Komorebic("move-to-workspace 5")
!+7::Komorebic("move-to-workspace 6")
!+8::Komorebic("move-to-workspace 7")

View File

@@ -1,76 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.bar.json",
"monitor": {
"index": 0,
"work_area_offset": {
"left": 0,
"top": 40,
"right": 0,
"bottom": 40
}
},
"font_family": "JetBrains Mono",
"theme": {
"palette": "Base16",
"name": "Ashes",
"accent": "Base0D"
},
"left_widgets": [
{
"Komorebi": {
"workspaces": {
"enable": true,
"hide_empty_workspaces": false
},
"layout": {
"enable": true
},
"focused_window": {
"enable": true,
"show_icon": true
}
}
}
],
"right_widgets": [
{
"Media": {
"enable": true
}
},
{
"Storage": {
"enable": true
}
},
{
"Memory": {
"enable": true
}
},
{
"Network": {
"enable": true,
"show_total_data_transmitted": true,
"show_network_activity": true
}
},
{
"Date": {
"enable": true,
"format": "DayDateMonthYear"
}
},
{
"Time": {
"enable": true,
"format": "TwentyFourHour"
}
},
{
"Battery": {
"enable": true
}
}
]
}

View File

@@ -1,25 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.29/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.24/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.yaml",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",
"default_workspace_padding": 20,
"default_container_padding": 20,
"border": true,
"border_width": 8,
"border_padding": 8,
"border_offset": -1,
"theme": {
"palette": "Base16",
"name": "Ashes",
"unfocused_border": "Base03",
"bar_accent": "Base0D"
},
"stackbar": {
"height": 40,
"mode": "OnStack",
"tabs": {
"width": 300
}
"active_window_border": false,
"active_window_border_colours": {
"single": "#42a5f5",
"stack": "#00a542",
"monocle": "#ff3399"
},
"monitors": [
{
@@ -43,14 +35,6 @@
{
"name": "V",
"layout": "Rows"
},
{
"name": "VI",
"layout": "Grid"
},
{
"name": "VII",
"layout": "RightMainVerticalStack"
}
]
}

View File

@@ -1,5 +1,3 @@
# v0.1.22
In addition to the [changelog](https://github.com/LGUG2Z/komorebi/releases/tag/v0.1.22) of new features and fixes,
please note the following changes from `v0.1.21` to adjust your configuration files accordingly.
@@ -51,8 +49,8 @@ A 1px border is drawn around the window edge. Users may see a gap for a single p
transparent edge - this is the windows themed edge, and is not present for all applications.
```json
{
{
"border_offset": 0,
"border_width": 1
}
```
```

View File

@@ -1,124 +0,0 @@
# Troubleshooting
## AutoHotKey executable not found
If you try to start komorebi with AHK using `komorebic start --ahk`, and you have
not installed AHK using `scoop`, you'll probably receive an error:
```text
Error: could not find autohotkey, please make sure it is installed before using the --ahk flag
```
Depending on how AHK is installed the executable on your system may have a
different name. In order to account for this, you may set the `KOMOREBI_AHK_EXE`
environment variable in your
[PowerShell profile](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7.4)
to match the name of the executable as it is found on your system.
After setting `KOMOREBI_AHK_EXE` make sure to either reload your PowerShell
profile or open a new terminal tab.
## Komorebi is unresponsive when the display wakes from sleep
This can happen in rare cases when your monitor state is not preserved after it
wakes from sleep.
### Problem
Your hotkeys in _whkd_ work, but it feels as if _komorebi_ knows nothing about
the previous state (you can't control previous windows, although newly launched ones
can be manipulated as normal).
### Solution
Some monitors, such as the Samsung G8/G9 (LED, Neo, OLED) have an _adaptive
sync_ or _variable refresh rate_ setting within the actual monitor OSD that can
disrupt how the device is persisted in the _komorebi_ state following suspension.
To fix this, please try to disable _Adaptive Sync_ or any other _VRR_ branded
alias by referring to the manufacturer's documentation.
!!! warning
Disabling VRR within Windows (e.g. _Nvidia Control Panel_) may work and can indeed
change the configuration you see within your monitor's OSD, but some monitors
will re-enable the setting regardless following suspension.
### Reproducing
Ensure _komorebi_ is in an operational state by executing `komorebic start` as
normal.
If _komorebi_ is already unresponsive, then please restart _komorebi_ first by
running `komorebic stop` and `komorebic start`.
1. **`komorebic state`**
```json
{
"monitors": {
"elements": [
{
"id": 65537,
"name": "DISPLAY1",
"device": "SAM71AA",
"device_id": "SAM71AA-5&a1a3e88&0&UID24834",
"size": {
"left": 0,
"top": 0,
"right": 5120,
"bottom": 1440
}
}
]
}
}
```
This appears to be fine -- _komorebi_ is aware of the device and associated
window handles.
2. **Let your display go to sleep.**
Simply turning the monitor off is not enough to reproduce the problem; you must
let Windows turn off the display itself.
To avoid waiting an eternity:
- _Control Panel_ -> _Hardware and Sound_ -> _Power Options_ -> _Edit Plan
Settings_
_Turn off the display: 1 minute_
Allow a minute for the display to reset, then once it actually shuts off
allow for any additional time as prompted by your monitor for the cycle to
complete.
3. **Wake your display again** by pressing any key.
_komorebi_ should now be unresponsive.
4. **`komorebic state`**
Don't stop _komorebi_ just yet.
Since it's unresponsive, you can open another shell instead to execute the above command.
```json
{
"monitors": {
"elements": [
{
"id": 65537,
"name": "DISPLAY1",
"device": null,
"device_id": null
}
]
}
}
```
We can see the _komorebi_ state is no longer associated with the previous
device: `null`, suggesting an issue when the display resumes from a suspended
state.

View File

@@ -10,9 +10,6 @@ alt + shift + o : komorebic reload-configuration
# alt + f : if ($wshell.AppActivate('Firefox') -eq $False) { start firefox }
# alt + b : if ($wshell.AppActivate('Chrome') -eq $False) { start chrome }
alt + q : komorebic close
alt + m : komorebic minimize
# Focus windows
alt + h : komorebic focus left
alt + j : komorebic focus down
@@ -59,18 +56,8 @@ alt + y : komorebic flip-layout vertical
alt + 1 : komorebic focus-workspace 0
alt + 2 : komorebic focus-workspace 1
alt + 3 : komorebic focus-workspace 2
alt + 4 : komorebic focus-workspace 3
alt + 5 : komorebic focus-workspace 4
alt + 6 : komorebic focus-workspace 5
alt + 7 : komorebic focus-workspace 6
alt + 8 : komorebic focus-workspace 7
# Move windows across workspaces
alt + shift + 1 : komorebic move-to-workspace 0
alt + shift + 2 : komorebic move-to-workspace 1
alt + shift + 3 : komorebic move-to-workspace 2
alt + shift + 4 : komorebic move-to-workspace 3
alt + shift + 5 : komorebic move-to-workspace 4
alt + shift + 6 : komorebic move-to-workspace 5
alt + shift + 7 : komorebic move-to-workspace 6
alt + shift + 8 : komorebic move-to-workspace 7

View File

@@ -16,14 +16,19 @@ fmt:
install-target target:
cargo +stable install --path {{ target }} --locked
prepare:
komorebic ahk-asc '~/.config/komorebi/applications.yaml'
komorebic pwsh-asc '~/.config/komorebi/applications.yaml'
cat '~/.config/komorebi/komorebi.generated.ps1' >komorebi.generated.ps1
cat '~/.config/komorebi/komorebi.generated.ahk' >komorebi.generated.ahk
install:
just install-target komorebic
just install-target komorebic-no-console
just install-target komorebi-gui
just install-target komorebi-bar
just install-target komorebi
run:
just install-target komorebic
cargo +stable run --bin komorebi --locked
warn $RUST_LOG="warn":
@@ -39,19 +44,13 @@ trace $RUST_LOG="trace":
just run
deadlock $RUST_LOG="trace":
just install-komorebic
cargo +stable run --bin komorebi --locked --features deadlock_detection
docgen:
cargo run --package komorebic -- docgen
komorebic docgen
Get-ChildItem -Path "docs/cli" -Recurse -File | ForEach-Object { (Get-Content $_.FullName) -replace 'Usage: ', 'Usage: komorebic.exe ' | Set-Content $_.FullName }
schemagen:
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
komorebic static-config-schema > schema.json
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\
rm -Force .\bar-config-docs\schema.html
mv .\bar-config-docs\schema.bar.html .\bar-config-docs\schema.html

View File

@@ -1,37 +0,0 @@
[package]
name = "komorebi-bar"
version = "0.1.30"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi-client = { path = "../komorebi-client" }
komorebi-themes = { path = "../komorebi-themes" }
chrono = { workspace = true }
clap = { workspace = true }
color-eyre = { workspace = true }
crossbeam-channel = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
eframe = { workspace = true }
egui-phosphor = "0.7"
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
netdev = "0.31"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
random_word = { version = "0.4", features = ["en"] }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
starship-battery = "0.10"
sysinfo = { workspace = true }
tracing = { workspace = true }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
windows = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }

View File

@@ -1,458 +0,0 @@
use crate::config::KomobarConfig;
use crate::config::KomobarTheme;
use crate::config::Position;
use crate::config::PositionConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiNotificationState;
use crate::process_hwnd;
use crate::widget::BarWidget;
use crate::widget::WidgetConfig;
use crate::BAR_HEIGHT;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_LEFT;
use crate::MONITOR_RIGHT;
use crate::MONITOR_TOP;
use crossbeam_channel::Receiver;
use eframe::egui::Align;
use eframe::egui::CentralPanel;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::FontData;
use eframe::egui::FontDefinitions;
use eframe::egui::FontFamily;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Layout;
use eframe::egui::Margin;
use eframe::egui::Style;
use eframe::egui::TextStyle;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use komorebi_client::KomorebiTheme;
use komorebi_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
use komorebi_themes::Catppuccin;
use komorebi_themes::CatppuccinValue;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub struct Komobar {
pub config: Arc<KomobarConfig>,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>,
pub rx_gui: Receiver<komorebi_client::Notification>,
pub rx_config: Receiver<KomobarConfig>,
pub bg_color: Rc<RefCell<Color32>>,
pub scale_factor: f32,
}
pub fn apply_theme(ctx: &Context, theme: KomobarTheme, bg_color: Rc<RefCell<Color32>>) {
match theme {
KomobarTheme::Catppuccin {
name: catppuccin,
accent: catppuccin_value,
} => match catppuccin {
Catppuccin::Frappe => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::FRAPPE.base);
}
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
},
KomobarTheme::Base16 {
name: base16,
accent: base16_value,
} => {
ctx.set_style(base16.style());
let base16_value = base16_value.unwrap_or_default();
let accent = base16_value.color32(base16);
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
});
bg_color.replace(base16.background());
}
}
}
impl Komobar {
pub fn apply_config(
&mut self,
ctx: &Context,
config: &KomobarConfig,
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
) {
MAX_LABEL_WIDTH.store(
config.max_label_width.unwrap_or(400.0) as i32,
Ordering::SeqCst,
);
if let Some(font_family) = &config.font_family {
tracing::info!("attempting to add custom font family: {font_family}");
Self::add_custom_font(ctx, font_family);
}
let position = config.position.clone().unwrap_or(PositionConfig {
start: Some(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
}),
end: Some(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
}),
});
if let Some(hwnd) = process_hwnd() {
let start = position.start.unwrap_or(Position {
x: MONITOR_LEFT.load(Ordering::SeqCst) as f32,
y: MONITOR_TOP.load(Ordering::SeqCst) as f32,
});
let end = position.end.unwrap_or(Position {
x: MONITOR_RIGHT.load(Ordering::SeqCst) as f32,
y: BAR_HEIGHT,
});
let rect = komorebi_client::Rect {
left: start.x as i32,
top: start.y as i32,
right: end.x as i32,
bottom: end.y as i32,
};
let window = komorebi_client::Window::from(hwnd);
match window.set_position(&rect, false) {
Ok(_) => {
tracing::info!("updated bar position");
}
Err(error) => {
tracing::error!("{}", error.to_string())
}
}
}
match config.theme {
Some(theme) => {
apply_theme(ctx, theme, self.bg_color.clone());
}
None => {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
);
let config = home_dir.join("komorebi.json");
match komorebi_client::StaticConfig::read(&config) {
Ok(config) => {
if let Some(theme) = config.theme {
apply_theme(ctx, KomobarTheme::from(theme), self.bg_color.clone());
let stack_accent = match theme {
KomorebiTheme::Catppuccin {
name, stack_border, ..
} => stack_border
.unwrap_or(CatppuccinValue::Green)
.color32(name.as_theme()),
KomorebiTheme::Base16 {
name, stack_border, ..
} => stack_border.unwrap_or(Base16Value::Base0B).color32(name),
};
if let Some(state) = &self.komorebi_notification_state {
state.borrow_mut().stack_accent = Some(stack_accent);
}
}
}
Err(_) => {
ctx.set_style(Style::default());
self.bg_color.replace(Style::default().visuals.panel_fill);
}
}
}
}
if let Some(font_size) = &config.font_size {
tracing::info!("attempting to set custom font size: {font_size}");
Self::set_font_size(ctx, *font_size);
}
let mut komorebi_widget = None;
let mut komorebi_widget_idx = None;
let mut komorebi_notification_state = previous_notification_state;
let mut side = None;
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Side::Left);
}
}
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(config));
komorebi_widget_idx = Some(idx);
side = Some(Side::Right);
}
}
let mut left_widgets = config
.left_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
let mut right_widgets = config
.right_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
if let (Some(idx), Some(mut widget), Some(side)) =
(komorebi_widget_idx, komorebi_widget, side)
{
match komorebi_notification_state {
None => {
komorebi_notification_state = Some(widget.komorebi_notification_state.clone());
}
Some(ref previous) => {
previous
.borrow_mut()
.update_from_config(&widget.komorebi_notification_state.borrow());
widget.komorebi_notification_state = previous.clone();
}
}
let boxed: Box<dyn BarWidget> = Box::new(widget);
match side {
Side::Left => left_widgets[idx] = boxed,
Side::Right => right_widgets[idx] = boxed,
}
}
right_widgets.reverse();
self.left_widgets = left_widgets;
self.right_widgets = right_widgets;
tracing::info!("widget configuration options applied");
self.komorebi_notification_state = komorebi_notification_state;
}
pub fn new(
cc: &eframe::CreationContext<'_>,
rx_gui: Receiver<komorebi_client::Notification>,
rx_config: Receiver<KomobarConfig>,
config: Arc<KomobarConfig>,
) -> Self {
let mut komobar = Self {
config: config.clone(),
komorebi_notification_state: None,
left_widgets: vec![],
right_widgets: vec![],
rx_gui,
rx_config,
bg_color: Rc::new(RefCell::new(Style::default().visuals.panel_fill)),
scale_factor: cc.egui_ctx.native_pixels_per_point().unwrap_or(1.0),
};
komobar.apply_config(&cc.egui_ctx, &config, None);
// needs a double apply the first time for some reason
komobar.apply_config(&cc.egui_ctx, &config, None);
komobar
}
fn set_font_size(ctx: &Context, font_size: f32) {
ctx.style_mut(|style| {
style.text_styles = [
(TextStyle::Small, FontId::new(9.0, FontFamily::Proportional)),
(
TextStyle::Body,
FontId::new(font_size, FontFamily::Proportional),
),
(
TextStyle::Button,
FontId::new(font_size, FontFamily::Proportional),
),
(
TextStyle::Heading,
FontId::new(18.0, FontFamily::Proportional),
),
(
TextStyle::Monospace,
FontId::new(font_size, FontFamily::Monospace),
),
]
.into();
});
}
fn add_custom_font(ctx: &Context, name: &str) {
let mut fonts = FontDefinitions::default();
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
let property = FontPropertyBuilder::new().family(name).build();
if let Some((font, _)) = system_fonts::get(&property) {
fonts
.font_data
.insert(name.to_owned(), FontData::from_owned(font));
fonts
.families
.entry(FontFamily::Proportional)
.or_default()
.insert(0, name.to_owned());
fonts
.families
.entry(FontFamily::Monospace)
.or_default()
.push(name.to_owned());
// Tell egui to use these fonts:
ctx.set_fonts(fonts);
}
}
}
impl eframe::App for Komobar {
// TODO: I think this is needed for transparency??
// fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
// egui::Rgba::TRANSPARENT.to_array()
// let mut background = Color32::from_gray(18).to_normalized_gamma_f32();
// background[3] = 0.9;
// background
// }
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
self.apply_config(
ctx,
&self.config.clone(),
self.komorebi_notification_state.clone(),
);
}
if let Ok(updated_config) = self.rx_config.try_recv() {
self.apply_config(
ctx,
&updated_config,
self.komorebi_notification_state.clone(),
);
}
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
komorebi_notification_state
.borrow_mut()
.handle_notification(
ctx,
self.config.monitor.index,
self.rx_gui.clone(),
self.bg_color.clone(),
);
}
let frame = if let Some(frame) = &self.config.frame {
Frame::none()
.inner_margin(Margin::symmetric(
frame.inner_margin.x,
frame.inner_margin.y,
))
.fill(*self.bg_color.borrow())
} else {
Frame::none().fill(*self.bg_color.borrow())
};
CentralPanel::default().frame(frame).show(ctx, |ui| {
ui.horizontal_centered(|ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui);
}
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui);
}
})
})
});
}
}
#[derive(Copy, Clone)]
enum Side {
Left,
Right,
}

View File

@@ -1,160 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use starship_battery::units::ratio::percent;
use starship_battery::Manager;
use starship_battery::State;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct BatteryConfig {
/// Enable the Battery widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<BatteryConfig> for Battery {
fn from(value: BatteryConfig) -> Self {
let manager = Manager::new().unwrap();
let mut last_state = String::new();
let mut state = None;
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(mut batteries) = manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
match first.state() {
State::Charging => state = Some(BatteryState::Charging),
State::Discharging => state = Some(BatteryState::Discharging),
_ => {}
}
last_state = match prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
Self {
enable: value.enable,
manager,
last_state,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
state: state.unwrap_or(BatteryState::Discharging),
last_updated: Instant::now(),
}
}
}
pub enum BatteryState {
Charging,
Discharging,
}
pub struct Battery {
pub enable: bool,
manager: Manager,
pub state: BatteryState,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_state: String,
last_updated: Instant,
}
impl Battery {
fn output(&mut self) -> String {
let mut output = self.last_state.clone();
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
output.clear();
if let Ok(mut batteries) = self.manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
match first.state() {
State::Charging => self.state = BatteryState::Charging,
State::Discharging => self.state = BatteryState::Discharging,
_ => {}
}
output = match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
}
}
self.last_state.clone_from(&output);
self.last_updated = now;
}
output
}
}
impl BarWidget for Battery {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let emoji = match self.state {
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
};
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
ui.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
);
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,178 +0,0 @@
use crate::widget::WidgetConfig;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
use eframe::egui::Vec2;
use komorebi_client::KomorebiTheme;
use komorebi_client::Rect;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.30`
pub struct KomobarConfig {
/// Bar positioning options
#[serde(alias = "viewport")]
pub position: Option<PositionConfig>,
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// Monitor options
pub monitor: MonitorConfig,
/// Font family
pub font_family: Option<String>,
/// Font size (default: 12.5)
pub font_size: Option<f32>,
/// Max label width before text truncation (default: 400.0)
pub max_label_width: Option<f32>,
/// Theme
pub theme: Option<KomobarTheme>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Right side widgets (ordered left-to-right)
pub right_widgets: Vec<WidgetConfig>,
}
impl KomobarConfig {
pub fn aliases(raw: &str) {
let mut map = HashMap::new();
map.insert("position", ["viewport"]);
map.insert("end", ["inner_frame"]);
let mut display = false;
for aliases in map.values() {
for a in aliases {
if raw.contains(a) {
display = true;
}
}
}
if display {
println!("\nYour bar configuration file contains some options that have been renamed or deprecated:\n");
for (canonical, aliases) in map {
for alias in aliases {
if raw.contains(alias) {
println!(r#""{alias}" is now "{canonical}""#);
}
}
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct PositionConfig {
/// The desired starting position of the bar (0,0 = top left of the screen)
#[serde(alias = "position")]
pub start: Option<Position>,
/// The desired size of the bar from the starting position (usually monitor width x desired height)
#[serde(alias = "inner_size")]
pub end: Option<Position>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct FrameConfig {
/// Margin inside the painted frame
pub inner_margin: Position,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MonitorConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub index: usize,
/// Automatically apply a work area offset for this monitor to accommodate the bar
pub work_area_offset: Option<Rect>,
}
impl KomobarConfig {
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = match path.extension().unwrap().to_string_lossy().as_str() {
"json" => serde_json::from_str(&content)?,
_ => panic!("unsupported format"),
};
if value.frame.is_none() {
value.frame = Some(FrameConfig {
inner_margin: Position { x: 10.0, y: 10.0 },
});
}
Ok(value)
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct Position {
/// X coordinate
pub x: f32,
/// Y coordinate
pub y: f32,
}
impl From<Position> for Vec2 {
fn from(value: Position) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
impl From<Position> for Pos2 {
fn from(value: Position) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "palette")]
pub enum KomobarTheme {
/// A theme from catppuccin-egui
Catppuccin {
name: komorebi_themes::Catppuccin,
accent: Option<komorebi_themes::CatppuccinValue>,
},
/// A theme from base16-egui-themes
Base16 {
name: komorebi_themes::Base16,
accent: Option<komorebi_themes::Base16Value>,
},
}
impl From<KomorebiTheme> for KomobarTheme {
fn from(value: KomorebiTheme) -> Self {
match value {
KomorebiTheme::Catppuccin {
name, bar_accent, ..
} => Self::Catppuccin {
name,
accent: bar_accent,
},
KomorebiTheme::Base16 {
name, bar_accent, ..
} => Self::Base16 {
name,
accent: bar_accent,
},
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum LabelPrefix {
/// Show no prefix
None,
/// Show an icon
Icon,
/// Show text
Text,
/// Show an icon and text
IconAndText,
}

View File

@@ -1,120 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct CpuConfig {
/// Enable the Cpu widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<CpuConfig> for Cpu {
fn from(value: CpuConfig) -> Self {
let mut system =
System::new_with_specifics(RefreshKind::default().without_memory().without_processes());
system.refresh_cpu_usage();
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Cpu {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Cpu {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_cpu_usage();
self.last_updated = now;
}
let used = self.system.global_cpu_usage();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
}
}
}
impl BarWidget for Cpu {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CPU.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,137 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct DateConfig {
/// Enable the Date widget
pub enable: bool,
/// Set the Date format
pub format: DateFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<DateConfig> for Date {
fn from(value: DateConfig) -> Self {
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum DateFormat {
/// Month/Date/Year format (09/08/24)
MonthDateYear,
/// Year-Month-Date format (2024-09-08)
YearMonthDate,
/// Date-Month-Year format (8-Sep-2024)
DateMonthYear,
/// Day Date Month Year format (8 September 2024)
DayDateMonthYear,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
Custom(String),
}
impl DateFormat {
pub fn next(&mut self) {
match self {
DateFormat::MonthDateYear => *self = Self::YearMonthDate,
DateFormat::YearMonthDate => *self = Self::DateMonthYear,
DateFormat::DateMonthYear => *self = Self::DayDateMonthYear,
DateFormat::DayDateMonthYear => *self = Self::MonthDateYear,
_ => {}
};
}
fn fmt_string(&self) -> String {
match self {
DateFormat::MonthDateYear => String::from("%D"),
DateFormat::YearMonthDate => String::from("%F"),
DateFormat::DateMonthYear => String::from("%v"),
DateFormat::DayDateMonthYear => String::from("%A %e %B %Y"),
DateFormat::Custom(custom) => custom.to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct Date {
pub enable: bool,
pub format: DateFormat,
label_prefix: LabelPrefix,
}
impl Date {
fn output(&mut self) -> String {
chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()
}
}
impl BarWidget for Date {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let mut output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CALENDAR_DOTS.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.insert_str(0, "DATE: ");
}
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(WidgetText::LayoutJob(layout_job.clone()))
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.next()
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,543 +0,0 @@
use crate::bar::apply_theme;
use crate::config::KomobarTheme;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use crate::WIDGET_SPACING;
use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError;
use eframe::egui::text::LayoutJob;
use eframe::egui::Color32;
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::SelectableLabel;
use eframe::egui::Sense;
use eframe::egui::TextStyle;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use image::RgbaImage;
use komorebi_client::CycleDirection;
use komorebi_client::NotificationEvent;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::fmt::Formatter;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: KomorebiWorkspacesConfig,
/// Configure the Layout widget
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Focused Window widget
pub focused_window: Option<KomorebiFocusedWindowConfig>,
/// Configure the Configuration Switcher widget
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiWorkspacesConfig {
/// Enable the Komorebi Workspaces widget
pub enable: bool,
/// Hide workspaces without any windows
pub hide_empty_workspaces: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiFocusedWindowConfig {
/// Enable the Komorebi Focused Window widget
pub enable: bool,
/// Show the icon of the currently focused window
pub show_icon: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfigurationSwitcherConfig {
/// Enable the Komorebi Configurations widget
pub enable: bool,
/// A map of display friendly name => path to configuration.json
pub configurations: BTreeMap<String, String>,
}
impl From<&KomorebiConfig> for Komorebi {
fn from(value: &KomorebiConfig) -> Self {
let configuration_switcher =
if let Some(configuration_switcher) = &value.configuration_switcher {
let mut configuration_switcher = configuration_switcher.clone();
for (_, location) in configuration_switcher.configurations.iter_mut() {
*location = dunce::simplified(&PathBuf::from(location.clone()))
.to_string_lossy()
.to_string();
}
Some(configuration_switcher)
} else {
None
};
Self {
komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState {
selected_workspace: String::new(),
layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
workspaces: vec![],
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
mouse_follows_focus: true,
work_area_offset: None,
focused_container_information: (vec![], vec![], 0),
stack_accent: None,
})),
workspaces: value.workspaces,
layout: value.layout,
focused_window: value.focused_window,
configuration_switcher,
}
}
}
#[derive(Clone, Debug)]
pub struct Komorebi {
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
pub workspaces: KomorebiWorkspacesConfig,
pub layout: Option<KomorebiLayoutConfig>,
pub focused_window: Option<KomorebiFocusedWindowConfig>,
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
impl BarWidget for Komorebi {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
if self.workspaces.enable {
let mut update = None;
for (i, ws) in komorebi_notification_state.workspaces.iter().enumerate() {
if ui
.add(SelectableLabel::new(
komorebi_notification_state.selected_workspace.eq(ws),
ws.to_string(),
))
.clicked()
{
update = Some(ws.to_string());
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
.is_err()
{
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i))
.is_err()
{
tracing::error!("could not send message to komorebi: FocusWorkspaceNumber");
proceed = false;
}
if proceed
&& komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!("could not send message to komorebi: MouseFollowsFocus");
proceed = false;
}
if proceed && komorebi_client::send_message(&SocketMessage::Retile).is_err() {
tracing::error!("could not send message to komorebi: Retile");
}
}
}
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
ui.add_space(WIDGET_SPACING);
}
if let Some(layout) = self.layout {
if layout.enable {
if ui
.add(
Label::new(komorebi_notification_state.layout.to_string())
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
match komorebi_notification_state.layout {
KomorebiLayout::Default(_) => {
if komorebi_client::send_message(&SocketMessage::CycleLayout(
CycleDirection::Next,
))
.is_err()
{
tracing::error!("could not send message to komorebi: CycleLayout");
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err()
{
tracing::error!("could not send message to komorebi: ToggleTiling");
}
}
KomorebiLayout::Paused => {
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
tracing::error!("could not send message to komorebi: TogglePause");
}
}
KomorebiLayout::Custom => {}
}
}
ui.add_space(WIDGET_SPACING);
}
}
if let Some(configuration_switcher) = &self.configuration_switcher {
if configuration_switcher.enable {
for (name, location) in configuration_switcher.configurations.iter() {
let path = PathBuf::from(location);
if path.is_file()
&& ui
.add(Label::new(name).selectable(false).sense(Sense::click()))
.clicked()
{
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
canonicalized,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: ReplaceConfiguration"
);
proceed = false;
}
if let Some(rect) = komorebi_notification_state.work_area_offset {
if proceed {
match komorebi_client::send_query(&SocketMessage::Query(
komorebi_client::StateQuery::FocusedMonitorIndex,
)) {
Ok(idx) => {
if let Ok(monitor_idx) = idx.parse::<usize>() {
if komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
rect,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: MonitorWorkAreaOffset"
);
}
}
}
Err(_) => {
tracing::error!(
"could not send message to komorebi: Query"
);
}
}
}
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
if let Some(focused_window) = self.focused_window {
if focused_window.enable {
let titles = &komorebi_notification_state.focused_container_information.0;
let icons = &komorebi_notification_state.focused_container_information.1;
let focused_window_idx =
komorebi_notification_state.focused_container_information.2;
let iter = titles.iter().zip(icons.iter());
for (i, (title, icon)) in iter.enumerate() {
if focused_window.show_icon {
if let Some(img) = icon {
ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.max_height(15.0),
);
}
}
if i == focused_window_idx {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let layout_job = LayoutJob::simple(
title.to_string(),
font_id.clone(),
komorebi_notification_state
.stack_accent
.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
if titles.len() > 1 {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job).selectable(false).truncate(),
);
} else {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
custom_ui.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title).selectable(false).truncate(),
);
}
} else {
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(title)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
{
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
false,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
if komorebi_client::send_message(&SocketMessage::FocusStackWindow(i))
.is_err()
{
tracing::error!(
"could not send message to komorebi: FocusStackWindow"
);
}
if komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(
komorebi_notification_state.mouse_follows_focus,
))
.is_err()
{
tracing::error!(
"could not send message to komorebi: MouseFollowsFocus"
);
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
ui.add_space(WIDGET_SPACING);
}
}
}
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
ctx.load_texture("icon", color_image, TextureOptions::default())
}
#[derive(Clone, Debug)]
pub struct KomorebiNotificationState {
pub workspaces: Vec<String>,
pub selected_workspace: String,
pub focused_container_information: (Vec<String>, Vec<Option<RgbaImage>>, usize),
pub layout: KomorebiLayout,
pub hide_empty_workspaces: bool,
pub mouse_follows_focus: bool,
pub work_area_offset: Option<Rect>,
pub stack_accent: Option<Color32>,
}
#[derive(Copy, Clone, Debug)]
pub enum KomorebiLayout {
Default(komorebi_client::DefaultLayout),
Floating,
Paused,
Custom,
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
}
impl KomorebiNotificationState {
pub fn update_from_config(&mut self, config: &Self) {
self.hide_empty_workspaces = config.hide_empty_workspaces;
}
pub fn handle_notification(
&mut self,
ctx: &Context,
monitor_index: usize,
rx_gui: Receiver<komorebi_client::Notification>,
bg_color: Rc<RefCell<Color32>>,
) {
match rx_gui.try_recv() {
Err(error) => match error {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
tracing::error!(
"failed to receive komorebi notification on gui thread: {error}"
);
}
},
Ok(notification) => {
if let NotificationEvent::Socket(SocketMessage::ReloadStaticConfiguration(path)) =
notification.event
{
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
if let Some(theme) = config.theme {
apply_theme(ctx, KomobarTheme::from(theme), bg_color);
tracing::info!("applied theme from updated komorebi.json");
}
}
}
self.mouse_follows_focus = notification.state.mouse_follows_focus;
let monitor = &notification.state.monitors.elements()[monitor_index];
self.work_area_offset =
notification.state.monitors.elements()[monitor_index].work_area_offset();
let focused_workspace_idx = monitor.focused_workspace_idx();
let mut workspaces = vec![];
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
.name()
.to_owned()
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
for (i, ws) in monitor.workspaces().iter().enumerate() {
let should_add = if self.hide_empty_workspaces {
focused_workspace_idx == i || !ws.containers().is_empty()
} else {
true
};
if should_add {
workspaces
.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)));
}
}
self.workspaces = workspaces;
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(*layout),
komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
};
if !*monitor.workspaces()[focused_workspace_idx].tile() {
self.layout = KomorebiLayout::Floating;
}
if notification.state.is_paused {
self.layout = KomorebiLayout::Paused;
}
if let Some(container) =
monitor.workspaces()[focused_workspace_idx].monocle_container()
{
self.focused_container_information = (
container
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
container
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
container.focused_window_idx(),
);
} else if let Some(container) =
monitor.workspaces()[focused_workspace_idx].focused_container()
{
self.focused_container_information = (
container
.windows()
.iter()
.map(|w| w.title().unwrap_or_default())
.collect::<Vec<_>>(),
container
.windows()
.iter()
.map(|w| windows_icons::get_icon_by_process_id(w.process_id()))
.collect::<Vec<_>>(),
container.focused_window_idx(),
);
} else {
self.focused_container_information.0.clear();
self.focused_container_information.1.clear();
self.focused_container_information.2 = 0;
}
}
}
}
}

View File

@@ -1,411 +0,0 @@
mod bar;
mod battery;
mod config;
mod cpu;
mod date;
mod komorebi;
mod media;
mod memory;
mod network;
mod storage;
mod time;
mod ui;
mod widget;
use crate::bar::Komobar;
use crate::config::KomobarConfig;
use crate::config::Position;
use crate::config::PositionConfig;
use clap::Parser;
use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use schemars::gen::SchemaSettings;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext;
use windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2;
use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows;
use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
pub static WIDGET_SPACING: f32 = 10.0;
pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400);
pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0);
pub static MONITOR_RIGHT: AtomicI32 = AtomicI32::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
#[derive(Parser)]
#[clap(author, about, version)]
struct Opts {
/// Print the JSON schema of the configuration file and exit
#[clap(long)]
schema: bool,
/// Print a list of fonts available on this system and exit
#[clap(long)]
fonts: bool,
/// Path to a JSON or YAML configuration file
#[clap(short, long)]
config: Option<PathBuf>,
/// Write an example komorebi.bar.json to disk
#[clap(long)]
quickstart: bool,
/// Print a list of aliases that can be renamed to canonical variants
#[clap(long)]
#[clap(hide = true)]
aliases: bool,
}
extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
unsafe {
let mut process_id = 0;
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
if process_id == GetCurrentProcessId() {
*(lparam.0 as *mut HWND) = hwnd;
BOOL::from(false) // Stop enumeration
} else {
BOOL::from(true) // Continue enumeration
}
}
}
fn process_hwnd() -> Option<isize> {
unsafe {
let mut hwnd = HWND::default();
let _ = EnumThreadWindows(
GetCurrentThreadId(),
Some(enum_window),
LPARAM(&mut hwnd as *mut HWND as isize),
);
if hwnd.0 as isize == 0 {
None
} else {
Some(hwnd.0 as isize)
}
}
}
fn main() -> color_eyre::Result<()> {
unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }?;
let opts: Opts = Opts::parse();
if opts.schema {
let settings = SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
println!("{schema}");
std::process::exit(0);
}
if opts.fonts {
for font in system_fonts::query_all() {
println!("{font}");
}
std::process::exit(0);
}
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
color_eyre::install()?;
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
tracing::subscriber::set_global_default(
tracing_subscriber::fmt::Subscriber::builder()
.with_env_filter(EnvFilter::from_default_env())
.finish(),
)?;
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
);
if opts.quickstart {
let komorebi_bar_json = include_str!("../../docs/komorebi.bar.example.json").to_string();
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
println!(
"Example komorebi.bar.json file written to {}",
home_dir.as_path().display()
);
std::process::exit(0);
}
let default_config_path = home_dir.join("komorebi.bar.json");
let config_path = opts.config.map_or_else(
|| {
if !default_config_path.is_file() {
None
} else {
Some(default_config_path.clone())
}
},
Option::from,
);
let mut config = match config_path {
None => {
let komorebi_bar_json =
include_str!("../../docs/komorebi.bar.example.json").to_string();
std::fs::write(&default_config_path, komorebi_bar_json)?;
tracing::info!(
"created example configuration file: {}",
default_config_path.as_path().display()
);
KomobarConfig::read(&default_config_path)?
}
Some(ref config) => {
if !opts.aliases {
tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
}
KomobarConfig::read(config)?
}
};
let config_path = config_path.unwrap_or(default_config_path);
if opts.aliases {
KomobarConfig::aliases(&std::fs::read_to_string(&config_path)?);
std::process::exit(0);
}
let state = serde_json::from_str::<komorebi_client::State>(&komorebi_client::send_query(
&SocketMessage::State,
)?)?;
MONITOR_RIGHT.store(
state.monitors.elements()[config.monitor.index].size().right,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().top,
Ordering::SeqCst,
);
MONITOR_TOP.store(
state.monitors.elements()[config.monitor.index].size().left,
Ordering::SeqCst,
);
match config.position {
None => {
config.position = Some(PositionConfig {
start: Some(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
}),
end: Some(Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 50.0,
}),
})
}
Some(ref mut position) => {
if position.start.is_none() {
position.start = Some(Position {
x: state.monitors.elements()[config.monitor.index].size().left as f32,
y: state.monitors.elements()[config.monitor.index].size().top as f32,
});
}
if position.end.is_none() {
position.end = Some(Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 50.0,
})
}
}
}
let viewport_builder = ViewportBuilder::default()
.with_decorations(false)
// .with_transparent(config.transparent)
.with_taskbar(false);
let native_options = eframe::NativeOptions {
viewport: viewport_builder,
..Default::default()
};
if let Some(rect) = &config.monitor.work_area_offset {
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
config.monitor.index,
*rect,
))?;
tracing::info!(
"work area offset applied to monitor: {}",
config.monitor.index
);
}
let (tx_gui, rx_gui) = crossbeam_channel::unbounded();
let (tx_config, rx_config) = crossbeam_channel::unbounded();
let mut hotwatch = Hotwatch::new()?;
let config_path_cl = config_path.clone();
hotwatch.watch(config_path, move |event| match event.kind {
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
Ok(updated) => {
tracing::info!(
"configuration file updated: {}",
config_path_cl.as_path().to_string_lossy()
);
if let Err(error) = tx_config.send(updated) {
tracing::error!("could not send configuration update to gui: {error}")
}
}
Err(error) => {
tracing::error!("{error}");
}
},
_ => {}
})?;
tracing::info!("watching configuration file for changes");
let config_arc = Arc::new(config);
eframe::run_native(
"komorebi-bar",
native_options,
Box::new(|cc| {
let config_cl = config_arc.clone();
let ctx_repainter = cc.egui_ctx.clone();
std::thread::spawn(move || loop {
std::thread::sleep(Duration::from_secs(1));
ctx_repainter.request_repaint();
});
let ctx_komorebi = cc.egui_ctx.clone();
std::thread::spawn(move || {
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
filter_state_changes: true,
})
.expect("could not subscribe to komorebi notifications");
tracing::info!("subscribed to komorebi notifications: \"{}\"", subscriber_name);
for client in listener.incoming() {
match client {
Ok(subscription) => {
let mut buffer = Vec::new();
let mut reader = BufReader::new(subscription);
// this is when we know a shutdown has been sent
if matches!(reader.read_to_end(&mut buffer), Ok(0)) {
tracing::info!("disconnected from komorebi");
// keep trying to reconnect to komorebi
while komorebi_client::send_message(
&SocketMessage::AddSubscriberSocket(subscriber_name.clone()),
)
.is_err()
{
std::thread::sleep(Duration::from_secs(1));
}
tracing::info!("reconnected to komorebi");
if let Some(rect) = &config_cl.monitor.work_area_offset {
while komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
config_cl.monitor.index,
*rect,
),
)
.is_err()
{
std::thread::sleep(Duration::from_secs(1));
}
}
}
match String::from_utf8(buffer) {
Ok(notification_string) => {
match serde_json::from_str::<komorebi_client::Notification>(
&notification_string,
) {
Ok(notification) => {
tracing::debug!("received notification from komorebi");
if let Err(error) = tx_gui.send(notification) {
tracing::error!("could not send komorebi notification update to gui thread: {error}")
}
ctx_komorebi.request_repaint();
}
Err(error) => {
tracing::error!("could not deserialize komorebi notification: {error}");
}
}
}
Err(error) => {
tracing::error!(
"komorebi notification string was invalid utf8: {error}"
)
}
}
}
Err(error) => {
tracing::error!("{error}");
}
}
}
});
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config_arc)))
}),
)
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
}

View File

@@ -1,128 +0,0 @@
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,
}
impl From<MediaConfig> for Media {
fn from(value: MediaConfig) -> Self {
Self::new(value.enable)
}
}
#[derive(Clone, Debug)]
pub struct Media {
pub enable: bool,
pub session_manager: GlobalSystemMediaTransportControlsSessionManager,
}
impl Media {
pub fn new(enable: bool) -> Self {
Self {
enable,
session_manager: GlobalSystemMediaTransportControlsSessionManager::RequestAsync()
.unwrap()
.get()
.unwrap(),
}
}
pub fn toggle(&self) {
if let Ok(session) = self.session_manager.GetCurrentSession() {
if let Ok(op) = session.TryTogglePlayPauseAsync() {
op.get().unwrap_or_default();
}
}
}
fn output(&mut self) -> String {
if let Ok(session) = self.session_manager.GetCurrentSession() {
if let Ok(operation) = session.TryGetMediaPropertiesAsync() {
if let Ok(properties) = operation.get() {
if let (Ok(artist), Ok(title)) = (properties.Artist(), properties.Title()) {
if artist.is_empty() {
return format!("{title}");
}
if title.is_empty() {
return format!("{artist}");
}
return format!("{artist} - {title}");
}
}
}
}
String::new()
}
}
impl BarWidget for Media {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
egui_phosphor::regular::HEADPHONES.to_string(),
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
let available_height = ui.available_height();
let mut custom_ui = CustomUi(ui);
if custom_ui
.add_sized_left_to_right(
Vec2::new(
MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32,
available_height,
),
Label::new(layout_job)
.selectable(false)
.sense(Sense::click())
.truncate(),
)
.clicked()
{
self.toggle();
}
ui.add_space(WIDGET_SPACING);
}
}
}
}

View File

@@ -1,123 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MemoryConfig {
/// Enable the Memory widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<MemoryConfig> for Memory {
fn from(value: MemoryConfig) -> Self {
let mut system =
System::new_with_specifics(RefreshKind::default().without_cpu().without_processes());
system.refresh_memory();
Self {
enable: value.enable,
system,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Memory {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Memory {
fn output(&mut self) -> String {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_memory();
self.last_updated = now;
}
let used = self.system.used_memory();
let total = self.system.total_memory();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {}%", (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
}
}
}
impl BarWidget for Memory {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::MEMORY.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
{
eprintln!("{}", error)
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,425 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use num_derive::FromPrimitive;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show total data transmitted
pub show_total_data_transmitted: bool,
/// Show network activity
pub show_network_activity: bool,
/// Characters to reserve for network activity data
pub network_activity_fill_characters: Option<usize>,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let mut last_state_data = vec![];
let mut last_state_transmitted = vec![];
let mut networks_total_data_transmitted = Networks::new_with_refreshed_list();
let mut networks_network_activity = Networks::new_with_refreshed_list();
let mut default_interface = String::new();
let prefix = value.label_prefix.unwrap_or(LabelPrefix::Icon);
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = interface.friendly_name {
default_interface.clone_from(&friendly_name);
if value.show_total_data_transmitted {
networks_total_data_transmitted.refresh();
for (interface_name, data) in &networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
last_state_data.push(match prefix {
LabelPrefix::None => format!(
"{} | {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Icon => format!(
"{} {} | {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Text => format!(
"\u{2211}DOWN: {} | \u{2211}UP: {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::IconAndText => format!(
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
})
}
}
}
if value.show_network_activity {
networks_network_activity.refresh();
for (interface_name, data) in &networks_network_activity {
if friendly_name.eq(interface_name) {
last_state_transmitted.push(match prefix {
LabelPrefix::None => format!(
"{: >width$}/s | {: >width$}/s",
to_pretty_bytes(data.received(), 1),
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::Icon => format!(
"{} {: >width$}/s | {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::Text => format!(
"DOWN: {: >width$}/s | UP: {: >width$}/s",
to_pretty_bytes(data.received(), 1),
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
LabelPrefix::IconAndText => format!(
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.transmitted(), 1),
width =
value.network_activity_fill_characters.unwrap_or_default(),
),
})
}
}
}
}
}
Self {
enable: value.enable,
networks_total_data_transmitted,
networks_network_activity,
default_interface,
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: prefix,
show_total_data_transmitted: value.show_total_data_transmitted,
show_network_activity: value.show_network_activity,
network_activity_fill_characters: value
.network_activity_fill_characters
.unwrap_or_default(),
last_state_total_data_transmitted: last_state_data,
last_state_network_activity: last_state_transmitted,
last_updated_total_data_transmitted: Instant::now(),
last_updated_network_activity: Instant::now(),
}
}
}
pub struct Network {
pub enable: bool,
pub show_total_data_transmitted: bool,
pub show_network_activity: bool,
networks_total_data_transmitted: Networks,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
default_interface: String,
last_state_total_data_transmitted: Vec<String>,
last_state_network_activity: Vec<String>,
last_updated_total_data_transmitted: Instant,
last_updated_network_activity: Instant,
network_activity_fill_characters: usize,
}
impl Network {
fn default_interface(&mut self) {
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
}
}
}
fn network_activity(&mut self) -> Vec<String> {
let mut outputs = self.last_state_network_activity.clone();
let now = Instant::now();
if self.show_network_activity
&& now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_network_activity {
self.networks_network_activity.refresh();
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
outputs.push(match self.label_prefix {
LabelPrefix::None => format!(
"{: >width$}/s | {: >width$}/s",
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::Icon => format!(
"{} {: >width$}/s | {} {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::Text => format!(
"DOWN: {: >width$}/s | UP: {: >width$}/s",
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
),
LabelPrefix::IconAndText => {
format!(
"{} DOWN: {: >width$}/s | {} UP: {: >width$}/s",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(
data.received(),
self.data_refresh_interval
),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval
),
width = self.network_activity_fill_characters,
)
}
})
}
}
}
}
}
self.last_state_network_activity.clone_from(&outputs);
self.last_updated_network_activity = now;
}
outputs
}
fn total_data_transmitted(&mut self) -> Vec<String> {
let mut outputs = self.last_state_total_data_transmitted.clone();
let now = Instant::now();
if self.show_total_data_transmitted
&& now.duration_since(self.last_updated_total_data_transmitted)
> Duration::from_secs(self.data_refresh_interval)
{
outputs.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
if self.show_total_data_transmitted {
self.networks_total_data_transmitted.refresh();
for (interface_name, data) in &self.networks_total_data_transmitted {
if friendly_name.eq(interface_name) {
outputs.push(match self.label_prefix {
LabelPrefix::None => format!(
"{} | {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Icon => format!(
"{} {} | {} {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::Text => format!(
"\u{2211}DOWN: {} | \u{2211}UP: {}",
to_pretty_bytes(data.total_received(), 1),
to_pretty_bytes(data.total_transmitted(), 1),
),
LabelPrefix::IconAndText => format!(
"{} \u{2211}DOWN: {} | {} \u{2211}UP: {}",
egui_phosphor::regular::ARROW_FAT_DOWN,
to_pretty_bytes(data.total_received(), 1),
egui_phosphor::regular::ARROW_FAT_UP,
to_pretty_bytes(data.total_transmitted(), 1),
),
})
}
}
}
}
}
self.last_state_total_data_transmitted.clone_from(&outputs);
self.last_updated_total_data_transmitted = now;
}
outputs
}
}
impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.show_total_data_transmitted {
for output in self.total_data_transmitted() {
ui.add(Label::new(output).selectable(false));
}
ui.add_space(WIDGET_SPACING);
}
if self.show_network_activity {
for output in self.network_activity() {
ui.add(Label::new(output).selectable(false));
}
ui.add_space(WIDGET_SPACING);
}
if self.enable {
self.default_interface();
if !self.default_interface.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error)
}
}
}
ui.add_space(WIDGET_SPACING);
}
}
}
#[derive(Debug, FromPrimitive)]
enum DataUnit {
B = 0,
K = 1,
M = 2,
G = 3,
T = 4,
P = 5,
E = 6,
Z = 7,
Y = 8,
}
impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
}

View File

@@ -1,134 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Disks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct StorageConfig {
/// Enable the Storage widget
pub enable: bool,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<StorageConfig> for Storage {
fn from(value: StorageConfig) -> Self {
Self {
enable: value.enable,
disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
last_updated: Instant::now(),
}
}
}
pub struct Storage {
pub enable: bool,
disks: Disks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_updated: Instant,
}
impl Storage {
fn output(&mut self) -> Vec<String> {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.disks.refresh();
self.last_updated = now;
}
let mut disks = vec![];
for disk in &self.disks {
let mount = disk.mount_point();
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
disks.push(match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
})
}
disks.sort();
disks.reverse();
disks
}
}
impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
for output in self.output() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::HARD_DRIVES.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id.clone(), ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
if let Err(error) = Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
output.split(' ').collect::<Vec<&str>>()[0],
])
.spawn()
{
eprintln!("{}", error)
}
}
ui.add_space(WIDGET_SPACING);
}
}
}
}

View File

@@ -1,128 +0,0 @@
use crate::config::LabelPrefix;
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::text::LayoutJob;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::TextFormat;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct TimeConfig {
/// Enable the Time widget
pub enable: bool,
/// Set the Time format
pub format: TimeFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<TimeConfig> for Time {
fn from(value: TimeConfig) -> Self {
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum TimeFormat {
/// Twelve-hour format (with seconds)
TwelveHour,
/// Twenty-four-hour format (with seconds)
TwentyFourHour,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
Custom(String),
}
impl TimeFormat {
pub fn toggle(&mut self) {
match self {
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
_ => {}
};
}
fn fmt_string(&self) -> String {
match self {
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
TimeFormat::TwentyFourHour => String::from("%T"),
TimeFormat::Custom(format) => format.to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct Time {
pub enable: bool,
pub format: TimeFormat,
label_prefix: LabelPrefix,
}
impl Time {
fn output(&mut self) -> String {
chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()
}
}
impl BarWidget for Time {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let mut output = self.output();
if !output.is_empty() {
let font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CLOCK.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.insert_str(0, "TIME: ");
}
layout_job.append(
&output,
10.0,
TextFormat::simple(font_id, ctx.style().visuals.text_color()),
);
if ui
.add(
Label::new(layout_job)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
self.format.toggle()
}
}
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,22 +0,0 @@
use eframe::egui::Align;
use eframe::egui::Layout;
use eframe::egui::Response;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::Widget;
pub struct CustomUi<'ui>(pub &'ui mut Ui);
impl CustomUi<'_> {
pub fn add_sized_left_to_right(
&mut self,
max_size: impl Into<Vec2>,
widget: impl Widget,
) -> Response {
let layout =
Layout::from_main_dir_and_cross_align(self.0.layout().main_dir(), Align::Center);
self.0
.allocate_ui_with_layout(max_size.into(), layout, |ui| ui.add(widget))
.inner
}
}

View File

@@ -1,56 +0,0 @@
use crate::battery::Battery;
use crate::battery::BatteryConfig;
use crate::cpu::Cpu;
use crate::cpu::CpuConfig;
use crate::date::Date;
use crate::date::DateConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiConfig;
use crate::media::Media;
use crate::media::MediaConfig;
use crate::memory::Memory;
use crate::memory::MemoryConfig;
use crate::network::Network;
use crate::network::NetworkConfig;
use crate::storage::Storage;
use crate::storage::StorageConfig;
use crate::time::Time;
use crate::time::TimeConfig;
use eframe::egui::Context;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
pub trait BarWidget {
fn render(&mut self, ctx: &Context, ui: &mut Ui);
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum WidgetConfig {
Battery(BatteryConfig),
Cpu(CpuConfig),
Date(DateConfig),
Komorebi(KomorebiConfig),
Media(MediaConfig),
Memory(MemoryConfig),
Network(NetworkConfig),
Storage(StorageConfig),
Time(TimeConfig),
}
impl WidgetConfig {
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
match self {
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(config)),
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
}
}
}

View File

@@ -1,12 +1,12 @@
[package]
name = "komorebi-client"
version = "0.1.30"
version = "0.1.25-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
komorebi = { path = "../komorebi" }
uds_windows = { workspace = true }
komorebi-core = { path = "../komorebi-core" }
uds_windows = "1"
serde_json = { workspace = true }

View File

@@ -1,51 +1,25 @@
#![warn(clippy::all)]
#![warn(clippy::all, clippy::nursery, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::config_generation::ApplicationConfiguration;
pub use komorebi::container::Container;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::resolve_home_path;
pub use komorebi::core::AnimationStyle;
pub use komorebi::core::ApplicationIdentifier;
pub use komorebi::core::Arrangement;
pub use komorebi::core::Axis;
pub use komorebi::core::BorderImplementation;
pub use komorebi::core::BorderStyle;
pub use komorebi::core::CustomLayout;
pub use komorebi::core::CycleDirection;
pub use komorebi::core::DefaultLayout;
pub use komorebi::core::Direction;
pub use komorebi::core::FocusFollowsMouseImplementation;
pub use komorebi::core::HidingBehaviour;
pub use komorebi::core::Layout;
pub use komorebi::core::MoveBehaviour;
pub use komorebi::core::OperationBehaviour;
pub use komorebi::core::OperationDirection;
pub use komorebi::core::Rect;
pub use komorebi::core::Sizing;
pub use komorebi::core::SocketMessage;
pub use komorebi::core::StackbarLabel;
pub use komorebi::core::StackbarMode;
pub use komorebi::core::StateQuery;
pub use komorebi::core::WindowKind;
pub use komorebi::monitor::Monitor;
pub use komorebi::ring::Ring;
pub use komorebi::window::Window;
pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace;
pub use komorebi::BorderColours;
pub use komorebi::GlobalState;
pub use komorebi::KomorebiTheme;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;
pub use komorebi::StaticConfig;
pub use komorebi::SubscribeOptions;
pub use komorebi::TabsConfig;
pub use komorebi_core::Arrangement;
pub use komorebi_core::Axis;
pub use komorebi_core::CustomLayout;
pub use komorebi_core::CycleDirection;
pub use komorebi_core::DefaultLayout;
pub use komorebi_core::Direction;
pub use komorebi_core::Layout;
pub use komorebi_core::OperationDirection;
pub use komorebi_core::Rect;
pub use komorebi_core::SocketMessage;
use komorebi::DATA_DIR;
@@ -60,10 +34,16 @@ const KOMOREBI: &str = "komorebi.sock";
pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
let socket = DATA_DIR.join(KOMOREBI);
let mut stream = UnixStream::connect(socket)?;
stream.write_all(serde_json::to_string(message)?.as_bytes())
}
let mut connected = false;
while !connected {
if let Ok(mut stream) = UnixStream::connect(&socket) {
connected = true;
stream.write_all(serde_json::to_string(message)?.as_bytes())?;
}
}
Ok(())
}
pub fn send_query(message: &SocketMessage) -> std::io::Result<String> {
let socket = DATA_DIR.join(KOMOREBI);
@@ -97,29 +77,3 @@ pub fn subscribe(name: &str) -> std::io::Result<UnixListener> {
Ok(listener)
}
pub fn subscribe_with_options(
name: &str,
options: SubscribeOptions,
) -> std::io::Result<UnixListener> {
let socket = DATA_DIR.join(name);
match std::fs::remove_file(&socket) {
Ok(()) => {}
Err(error) => match error.kind() {
std::io::ErrorKind::NotFound => {}
_ => {
return Err(error);
}
},
};
let listener = UnixListener::bind(&socket)?;
send_message(&SocketMessage::AddSubscriberSocketWithOptions(
name.to_string(),
options,
))?;
Ok(listener)
}

18
komorebi-core/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "komorebi-core"
version = "0.1.25-dev.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = "0.9"
strum = { version = "0.26", features = ["derive"] }
schemars = "0.8"
color-eyre = { workspace = true }
windows = { workspace = true }
dunce = { workspace = true }
dirs = { workspace = true }

View File

@@ -7,12 +7,12 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use super::custom_layout::Column;
use super::custom_layout::ColumnSplit;
use super::custom_layout::ColumnSplitWithCapacity;
use super::CustomLayout;
use super::DefaultLayout;
use super::Rect;
use crate::custom_layout::Column;
use crate::custom_layout::ColumnSplit;
use crate::custom_layout::ColumnSplitWithCapacity;
use crate::CustomLayout;
use crate::DefaultLayout;
use crate::Rect;
pub trait Arrangement {
fn calculate(
@@ -26,7 +26,7 @@ pub trait Arrangement {
}
impl Arrangement for DefaultLayout {
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
#[allow(clippy::too_many_lines)]
fn calculate(
&self,
area: &Rect,
@@ -44,56 +44,8 @@ impl Arrangement for DefaultLayout {
layout_flip,
calculate_resize_adjustments(resize_dimensions),
),
Self::Columns => {
let mut layouts = columns(area, len);
let adjustment = calculate_columns_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
columns_reverse(&mut layouts);
}
}
layouts
}
Self::Rows => {
let mut layouts = rows(area, len);
let adjustment = calculate_rows_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
rows_reverse(&mut layouts);
}
}
layouts
}
Self::Columns => columns(area, len),
Self::Rows => rows(area, len),
Self::VerticalStack => {
let mut layouts: Vec<Rect> = vec![];
@@ -102,8 +54,16 @@ impl Arrangement for DefaultLayout {
_ => area.right / 2,
};
let main_left = area.left;
let stack_left = area.left + primary_right;
let mut main_left = area.left;
let mut stack_left = area.left + primary_right;
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
main_left = main_left + area.right - primary_right;
stack_left = area.left;
}
_ => {}
}
if len >= 1 {
layouts.push(Rect {
@@ -126,113 +86,6 @@ impl Arrangement for DefaultLayout {
}
}
let adjustment = calculate_vertical_stack_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
for rect in rest.iter_mut() {
rect.left = primary.left;
}
primary.left = rest[0].left + rest[0].right;
}
}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 3.. = len {
rows_reverse(&mut layouts[1..]);
}
}
layouts
}
Self::RightMainVerticalStack => {
// Shamelessly borrowed from LeftWM: https://github.com/leftwm/leftwm/commit/f673851745295ae7584a102535566f559d96a941
let mut layouts: Vec<Rect> = vec![];
let primary_width = match len {
1 => area.right,
_ => area.right / 2,
};
let primary_left = match len {
1 => 0,
_ => area.right - primary_width,
};
if len >= 1 {
layouts.push(Rect {
left: area.left + primary_left,
top: area.top,
right: primary_width,
bottom: area.bottom,
});
if len > 1 {
layouts.append(&mut rows(
&Rect {
left: area.left,
top: area.top,
right: primary_left,
bottom: area.bottom,
},
len - 1,
));
}
}
let adjustment = calculate_right_vertical_stack_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
primary.left = rest[0].left;
for rect in rest.iter_mut() {
rect.left = primary.left + primary.right;
}
}
}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 3.. = len {
rows_reverse(&mut layouts[1..]);
}
}
layouts
}
Self::HorizontalStack => {
@@ -243,8 +96,16 @@ impl Arrangement for DefaultLayout {
_ => area.bottom / 2,
};
let main_top = area.top;
let stack_top = area.top + bottom;
let mut main_top = area.top;
let mut stack_top = area.top + bottom;
match layout_flip {
Some(Axis::Vertical | Axis::HorizontalAndVertical) if len > 1 => {
main_top = main_top + area.bottom - bottom;
stack_top = area.top;
}
_ => {}
}
if len >= 1 {
layouts.push(Rect {
@@ -267,155 +128,9 @@ impl Arrangement for DefaultLayout {
}
}
let adjustment = calculate_horizontal_stack_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 2.. = len {
let (primary, rest) = layouts.split_at_mut(1);
let primary = &mut primary[0];
for rect in rest.iter_mut() {
rect.top = primary.top;
}
primary.top = rest[0].top + rest[0].bottom;
}
}
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
if let 3.. = len {
columns_reverse(&mut layouts[1..]);
}
}
layouts
}
Self::UltrawideVerticalStack => {
let mut layouts: Vec<Rect> = vec![];
let primary_right = match len {
1 => area.right,
_ => area.right / 2,
};
let secondary_right = match len {
1 => 0,
2 => area.right - primary_right,
_ => (area.right - primary_right) / 2,
};
let (primary_left, secondary_left, stack_left) = match len {
1 => (area.left, 0, 0),
2 => {
let primary = area.left + secondary_right;
let secondary = area.left;
(primary, secondary, 0)
}
_ => {
let primary = area.left + secondary_right;
let secondary = area.left;
let stack = area.left + primary_right + secondary_right;
(primary, secondary, stack)
}
};
if len >= 1 {
layouts.push(Rect {
left: primary_left,
top: area.top,
right: primary_right,
bottom: area.bottom,
});
if len >= 2 {
layouts.push(Rect {
left: secondary_left,
top: area.top,
right: secondary_right,
bottom: area.bottom,
});
if len > 2 {
layouts.append(&mut rows(
&Rect {
left: stack_left,
top: area.top,
right: secondary_right,
bottom: area.bottom,
},
len - 2,
));
}
}
}
let adjustment = calculate_ultrawide_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
if matches!(
layout_flip,
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
) {
match len {
2 => {
let (primary, secondary) = layouts.split_at_mut(1);
let primary = &mut primary[0];
let secondary = &mut secondary[0];
primary.left = secondary.left;
secondary.left = primary.left + primary.right;
}
3.. => {
let (primary, rest) = layouts.split_at_mut(1);
let (secondary, tertiary) = rest.split_at_mut(1);
let primary = &mut primary[0];
let secondary = &mut secondary[0];
for rect in tertiary.iter_mut() {
rect.left = secondary.left;
}
primary.left = tertiary[0].left + tertiary[0].right;
secondary.left = primary.left + primary.right;
}
_ => {}
}
}
if matches!(
layout_flip,
Some(Axis::Vertical | Axis::HorizontalAndVertical)
) {
if let 4.. = len {
rows_reverse(&mut layouts[2..]);
}
}
layouts
}
Self::UltrawideVerticalStack => ultrawide(area, len, layout_flip, resize_dimensions),
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
@@ -604,17 +319,9 @@ impl Arrangement for CustomLayout {
}
#[derive(
Clone,
Copy,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum Axis {
Horizontal,
Vertical,
@@ -663,22 +370,6 @@ fn rows(area: &Rect, len: usize) -> Vec<Rect> {
layouts
}
fn columns_reverse(columns: &mut [Rect]) {
let len = columns.len();
columns[len - 1].left = columns[0].left;
for i in (0..len - 1).rev() {
columns[i].left = columns[i + 1].left + columns[i + 1].right;
}
}
fn rows_reverse(rows: &mut [Rect]) {
let len = rows.len();
rows[len - 1].top = rows[0].top;
for i in (0..len - 1).rev() {
rows[i].top = rows[i + 1].top + rows[i + 1].bottom;
}
}
fn calculate_resize_adjustments(resize_dimensions: &[Option<Rect>]) -> Vec<Option<Rect>> {
let mut resize_adjustments = resize_dimensions.to_vec();
@@ -872,187 +563,6 @@ fn recursive_fibonacci(
}
}
fn calculate_columns_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
match len {
0 | 1 => (),
_ => {
for (i, rect) in resize_dimensions.iter().enumerate() {
if let Some(rect) = rect {
if i != 0 {
resize_right(&mut result[i - 1], rect.left);
resize_left(&mut result[i], rect.left);
}
if i != len - 1 {
resize_right(&mut result[i], rect.right);
resize_left(&mut result[i + 1], rect.right);
}
}
}
}
};
result
}
fn calculate_rows_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
match len {
0 | 1 => (),
_ => {
for (i, rect) in resize_dimensions.iter().enumerate() {
if let Some(rect) = rect {
if i != 0 {
resize_bottom(&mut result[i - 1], rect.top);
resize_top(&mut result[i], rect.top);
}
if i != len - 1 {
resize_bottom(&mut result[i], rect.bottom);
resize_top(&mut result[i + 1], rect.bottom);
}
}
}
}
};
result
}
fn calculate_vertical_stack_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
match len {
// One container can't be resized
0 | 1 => (),
_ => {
let (master, stack) = result.split_at_mut(1);
let primary = &mut master[0];
if let Some(resize) = resize_dimensions[0] {
resize_right(primary, resize.right);
for s in &mut *stack {
resize_left(s, resize.right);
}
}
// Handle stack on the right
for (i, rect) in resize_dimensions[1..].iter().enumerate() {
if let Some(rect) = rect {
resize_right(primary, rect.left);
stack
.iter_mut()
.for_each(|vertical_element| resize_left(vertical_element, rect.left));
// Containers in stack except first can be resized up displacing container
// above them
if i != 0 {
resize_bottom(&mut stack[i - 1], rect.top);
resize_top(&mut stack[i], rect.top);
}
// Containers in stack except last can be resized down displacing container
// below them
if i != stack.len() - 1 {
resize_bottom(&mut stack[i], rect.bottom);
resize_top(&mut stack[i + 1], rect.bottom);
}
}
}
}
};
result
}
fn calculate_right_vertical_stack_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
match len {
// One container can't be resized
0 | 1 => (),
_ => {
let (master, stack) = result.split_at_mut(1);
let primary = &mut master[0];
if let Some(resize) = resize_dimensions[0] {
resize_left(primary, resize.left);
for s in &mut *stack {
resize_right(s, resize.left);
}
}
// Handle stack on the left
for (i, rect) in resize_dimensions[1..].iter().enumerate() {
if let Some(rect) = rect {
resize_left(primary, rect.right);
stack
.iter_mut()
.for_each(|vertical_element| resize_right(vertical_element, rect.right));
// Containers in stack except first can be resized up displacing container
// above them
if i != 0 {
resize_bottom(&mut stack[i - 1], rect.top);
resize_top(&mut stack[i], rect.top);
}
// Containers in stack except last can be resized down displacing container
// below them
if i != stack.len() - 1 {
resize_bottom(&mut stack[i], rect.bottom);
resize_top(&mut stack[i + 1], rect.bottom);
}
}
}
}
};
result
}
fn calculate_horizontal_stack_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
match len {
0 | 1 => (),
_ => {
let (primary, rest) = result.split_at_mut(1);
let primary = &mut primary[0];
if let Some(resize_primary) = resize_dimensions[0] {
resize_bottom(primary, resize_primary.bottom);
for horizontal_element in &mut *rest {
resize_top(horizontal_element, resize_primary.bottom);
}
}
for (i, rect) in resize_dimensions[1..].iter().enumerate() {
if let Some(rect) = rect {
resize_bottom(primary, rect.top);
rest.iter_mut()
.for_each(|vertical_element| resize_top(vertical_element, rect.top));
if i != 0 {
resize_right(&mut rest[i - 1], rect.left);
resize_left(&mut rest[i], rect.left);
}
if i != rest.len() - 1 {
resize_right(&mut rest[i], rect.right);
resize_left(&mut rest[i + 1], rect.right);
}
}
}
}
};
result
}
fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
let len = resize_dimensions.len();
let mut result = vec![Rect::default(); len];
@@ -1143,3 +653,99 @@ fn resize_top(rect: &mut Rect, resize: i32) {
fn resize_bottom(rect: &mut Rect, resize: i32) {
rect.bottom += resize / 2;
}
fn ultrawide(
area: &Rect,
len: usize,
layout_flip: Option<Axis>,
resize_dimensions: &[Option<Rect>],
) -> Vec<Rect> {
let mut layouts: Vec<Rect> = vec![];
let primary_right = match len {
1 => area.right,
_ => area.right / 2,
};
let secondary_right = match len {
1 => 0,
2 => area.right - primary_right,
_ => (area.right - primary_right) / 2,
};
let (primary_left, secondary_left, stack_left) = match len {
1 => (area.left, 0, 0),
2 => {
let mut primary = area.left + secondary_right;
let mut secondary = area.left;
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
primary = area.left;
secondary = area.left + primary_right;
}
_ => {}
}
(primary, secondary, 0)
}
_ => {
let primary = area.left + secondary_right;
let mut secondary = area.left;
let mut stack = area.left + primary_right + secondary_right;
match layout_flip {
Some(Axis::Horizontal | Axis::HorizontalAndVertical) if len > 1 => {
secondary = area.left + primary_right + secondary_right;
stack = area.left;
}
_ => {}
}
(primary, secondary, stack)
}
};
if len >= 1 {
layouts.push(Rect {
left: primary_left,
top: area.top,
right: primary_right,
bottom: area.bottom,
});
if len >= 2 {
layouts.push(Rect {
left: secondary_left,
top: area.top,
right: secondary_right,
bottom: area.bottom,
});
if len > 2 {
layouts.append(&mut rows(
&Rect {
left: stack_left,
top: area.top,
right: secondary_right,
bottom: area.bottom,
},
len - 2,
));
}
}
}
let adjustment = calculate_ultrawide_adjustment(resize_dimensions);
layouts
.iter_mut()
.zip(adjustment.iter())
.for_each(|(layout, adjustment)| {
layout.top += adjustment.top;
layout.bottom += adjustment.bottom;
layout.left += adjustment.left;
layout.right += adjustment.right;
});
layouts
}

View File

@@ -6,7 +6,7 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use super::ApplicationIdentifier;
use crate::ApplicationIdentifier;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
@@ -59,14 +59,6 @@ pub enum MatchingRule {
Composite(Vec<IdWithIdentifier>),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceMatchingRule {
pub monitor_index: usize,
pub workspace_index: usize,
pub matching_rule: MatchingRule,
pub initial_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct IdWithIdentifier {
pub kind: ApplicationIdentifier,
@@ -116,8 +108,7 @@ pub struct ApplicationConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<ApplicationOptions>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "float_identifiers")]
pub ignore_identifiers: Option<Vec<MatchingRule>>,
pub float_identifiers: Option<Vec<MatchingRule>>,
}
impl ApplicationConfiguration {
@@ -188,7 +179,7 @@ impl ApplicationConfigurationGenerator {
let mut lines = vec![String::from("# Generated by komorebic.exe"), String::new()];
let mut ignore_rules = vec![];
let mut float_rules = vec![];
for app in cfgen {
lines.push(format!("# {}", app.name));
@@ -202,15 +193,15 @@ impl ApplicationConfigurationGenerator {
}
}
if let Some(ignore_identifiers) = app.ignore_identifiers {
for matching_rule in ignore_identifiers {
if let Some(float_identifiers) = app.float_identifiers {
for matching_rule in float_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule =
format!("komorebic.exe float-rule {} \"{}\"", float.kind, float.id);
// Don't want to send duped signals especially as configs get larger
if !ignore_rules.contains(&float_rule) {
ignore_rules.push(float_rule.clone());
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
// if let Some(comment) = float.comment {
// lines.push(format!("# {comment}"));
@@ -239,7 +230,7 @@ impl ApplicationConfigurationGenerator {
let mut lines = vec![String::from("; Generated by komorebic.exe"), String::new()];
let mut ignore_rules = vec![];
let mut float_rules = vec![];
for app in cfgen {
lines.push(format!("; {}", app.name));
@@ -253,8 +244,8 @@ impl ApplicationConfigurationGenerator {
}
}
if let Some(ignore_identifiers) = app.ignore_identifiers {
for matching_rule in ignore_identifiers {
if let Some(float_identifiers) = app.float_identifiers {
for matching_rule in float_identifiers {
if let MatchingRule::Simple(float) = matching_rule {
let float_rule = format!(
"RunWait('komorebic.exe float-rule {} \"{}\"', , \"Hide\")",
@@ -262,8 +253,8 @@ impl ApplicationConfigurationGenerator {
);
// Don't want to send duped signals especially as configs get larger
if !ignore_rules.contains(&float_rule) {
ignore_rules.push(float_rule.clone());
if !float_rules.contains(&float_rule) {
float_rules.push(float_rule.clone());
// if let Some(comment) = float.comment {
// lines.push(format!("; {comment}"));

View File

@@ -12,9 +12,9 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use super::Rect;
use crate::Rect;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct CustomLayout(Vec<Column>);
impl Deref for CustomLayout {
@@ -250,7 +250,7 @@ impl CustomLayout {
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "column", content = "configuration")]
pub enum Column {
Primary(Option<ColumnWidth>),
@@ -258,18 +258,18 @@ pub enum Column {
Tertiary(ColumnSplit),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ColumnWidth {
WidthPercentage(f32),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ColumnSplit {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
pub enum ColumnSplitWithCapacity {
Horizontal(usize),
Vertical(usize),

View File

@@ -10,6 +10,7 @@ use strum::EnumString;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum CycleDirection {
Previous,
Next,

View File

@@ -5,23 +5,14 @@ use serde::Serialize;
use strum::Display;
use strum::EnumString;
use super::OperationDirection;
use super::Rect;
use super::Sizing;
use crate::OperationDirection;
use crate::Rect;
use crate::Sizing;
#[derive(
Clone,
Copy,
Debug,
Serialize,
Deserialize,
Eq,
PartialEq,
Display,
EnumString,
ValueEnum,
JsonSchema,
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum DefaultLayout {
BSP,
Columns,
@@ -30,42 +21,10 @@ pub enum DefaultLayout {
HorizontalStack,
UltrawideVerticalStack,
Grid,
RightMainVerticalStack,
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
}
impl DefaultLayout {
pub fn leftmost_index(&self, len: usize) -> usize {
match self {
Self::UltrawideVerticalStack | Self::RightMainVerticalStack => match len {
n if n > 1 => 1,
_ => 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,
}
}
#[must_use]
#[allow(clippy::cast_precision_loss, clippy::only_used_in_recursion)]
pub fn resize(
@@ -76,16 +35,7 @@ impl DefaultLayout {
sizing: Sizing,
delta: i32,
) -> Option<Rect> {
if !matches!(
self,
Self::BSP
| Self::Columns
| Self::Rows
| Self::VerticalStack
| Self::RightMainVerticalStack
| Self::HorizontalStack
| Self::UltrawideVerticalStack
) {
if !matches!(self, Self::BSP) && !matches!(self, Self::UltrawideVerticalStack) {
return None;
};
@@ -187,22 +137,20 @@ impl DefaultLayout {
Self::VerticalStack => Self::HorizontalStack,
Self::HorizontalStack => Self::UltrawideVerticalStack,
Self::UltrawideVerticalStack => Self::Grid,
Self::Grid => Self::RightMainVerticalStack,
Self::RightMainVerticalStack => Self::BSP,
Self::Grid => Self::BSP,
}
}
#[must_use]
pub const fn cycle_previous(self) -> Self {
match self {
Self::RightMainVerticalStack => Self::Grid,
Self::Grid => Self::UltrawideVerticalStack,
Self::BSP => 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,
Self::Columns => Self::Grid,
Self::Grid => Self::BSP,
}
}
}

View File

@@ -1,9 +1,9 @@
use super::custom_layout::Column;
use super::custom_layout::ColumnSplit;
use super::custom_layout::ColumnSplitWithCapacity;
use super::custom_layout::CustomLayout;
use super::DefaultLayout;
use super::OperationDirection;
use crate::custom_layout::Column;
use crate::custom_layout::ColumnSplit;
use crate::custom_layout::ColumnSplitWithCapacity;
use crate::custom_layout::CustomLayout;
use crate::DefaultLayout;
use crate::OperationDirection;
pub trait Direction {
fn index_in_direction(
@@ -90,45 +90,40 @@ impl Direction for DefaultLayout {
idx: usize,
count: usize,
) -> bool {
if count < 2 {
return false;
}
match op_direction {
OperationDirection::Up => match self {
Self::BSP => idx != 0 && idx != 1,
Self::BSP => count > 2 && idx != 0 && idx != 1,
Self::Columns => false,
Self::Rows | Self::HorizontalStack => idx != 0,
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
Self::VerticalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx > 2,
Self::Grid => !is_grid_edge(op_direction, idx, count),
},
OperationDirection::Down => match self {
Self::BSP => idx != count - 1 && idx % 2 != 0,
Self::BSP => count > 2 && idx != count - 1 && idx % 2 != 0,
Self::Columns => false,
Self::Rows => idx != count - 1,
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != count - 1,
Self::VerticalStack => 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),
},
OperationDirection::Left => match self {
Self::BSP => idx != 0,
Self::BSP => count > 1 && idx != 0,
Self::Columns | Self::VerticalStack => idx != 0,
Self::RightMainVerticalStack => idx == 0,
Self::Rows => false,
Self::HorizontalStack => idx != 0 && idx != 1,
Self::UltrawideVerticalStack => idx != 1,
Self::UltrawideVerticalStack => count > 1 && idx != 1,
Self::Grid => !is_grid_edge(op_direction, idx, count),
},
OperationDirection::Right => match self {
Self::BSP => idx % 2 == 0 && idx != count - 1,
Self::BSP => count > 1 && idx % 2 == 0 && idx != count - 1,
Self::Columns => idx != count - 1,
Self::Rows => false,
Self::VerticalStack => idx == 0,
Self::RightMainVerticalStack => idx != 0,
Self::HorizontalStack => idx != 0 && idx != count - 1,
Self::UltrawideVerticalStack => match count {
0 | 1 => false,
2 => idx != 0,
_ => idx < 2,
},
@@ -152,10 +147,7 @@ impl Direction for DefaultLayout {
}
}
Self::Columns => unreachable!(),
Self::Rows
| Self::VerticalStack
| Self::UltrawideVerticalStack
| Self::RightMainVerticalStack => idx - 1,
Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx - 1,
Self::HorizontalStack => 0,
Self::Grid => grid_neighbor(op_direction, idx, count),
}
@@ -168,11 +160,7 @@ impl Direction for DefaultLayout {
count: Option<usize>,
) -> usize {
match self {
Self::BSP
| Self::Rows
| Self::VerticalStack
| Self::UltrawideVerticalStack
| Self::RightMainVerticalStack => idx + 1,
Self::BSP | Self::Rows | Self::VerticalStack | Self::UltrawideVerticalStack => idx + 1,
Self::Columns => unreachable!(),
Self::HorizontalStack => 1,
Self::Grid => grid_neighbor(op_direction, idx, count),
@@ -196,7 +184,6 @@ impl Direction for DefaultLayout {
Self::Columns | Self::HorizontalStack => idx - 1,
Self::Rows => unreachable!(),
Self::VerticalStack => 0,
Self::RightMainVerticalStack => 1,
Self::UltrawideVerticalStack => match idx {
0 => 1,
1 => unreachable!(),
@@ -216,7 +203,6 @@ impl Direction for DefaultLayout {
Self::BSP | Self::Columns | Self::HorizontalStack => idx + 1,
Self::Rows => unreachable!(),
Self::VerticalStack => 1,
Self::RightMainVerticalStack => 0,
Self::UltrawideVerticalStack => match idx {
1 => 0,
0 => 2,

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